mirror of
https://github.com/pretix/pretix.git
synced 2026-01-01 18:32:27 +00:00
* Run exporters in repeatable read by default (Z#23173095) * Update src/pretix/helpers/database.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Rename parameter, add test * Do not run during tests --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
3185 lines
140 KiB
Python
3185 lines
140 KiB
Python
#
|
||
# This file is part of pretix (Community Edition).
|
||
#
|
||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||
#
|
||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||
# this file, see <https://pretix.eu/about/en/license>.
|
||
#
|
||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||
# details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||
# <https://www.gnu.org/licenses/>.
|
||
#
|
||
|
||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||
#
|
||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||
#
|
||
# This file contains Apache-licensed contributions copyrighted by: Christopher Dambamuromo, Enrique Saez, Tobias Kunze
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
# License for the specific language governing permissions and limitations under the License.
|
||
|
||
import datetime
|
||
import sys
|
||
import time
|
||
import zoneinfo
|
||
from datetime import date, timedelta
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
from dateutil.tz import tzoffset
|
||
from django.conf import settings
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.files.storage import default_storage
|
||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||
from django.test import TestCase
|
||
from django.utils.timezone import now
|
||
from django_scopes import scope, scopes_disabled
|
||
from freezegun import freeze_time
|
||
|
||
from pretix.base.i18n import language
|
||
from pretix.base.models import (
|
||
CachedFile, CartPosition, Checkin, CheckinList, Event, Item, ItemCategory,
|
||
ItemVariation, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||
Organizer, Question, Quota, ScheduledEventExport, SeatingPlan, User,
|
||
Voucher, WaitingListEntry,
|
||
)
|
||
from pretix.base.models.event import SubEvent
|
||
from pretix.base.models.items import (
|
||
ItemBundle, SubEventItem, SubEventItemVariation,
|
||
)
|
||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||
from pretix.base.services.orders import OrderError, cancel_order, perform_order
|
||
from pretix.base.services.quotas import QuotaAvailability
|
||
from pretix.helpers import repeatable_reads_transaction
|
||
from pretix.testutils.scope import classscope
|
||
|
||
|
||
class UserTestCase(TestCase):
|
||
def test_name(self):
|
||
u = User.objects.create_user('test@foo.bar', 'test')
|
||
u.fullname = "Christopher Nolan"
|
||
u.set_password("test")
|
||
u.save()
|
||
self.assertEqual(u.get_full_name(), 'Christopher Nolan')
|
||
self.assertEqual(u.get_short_name(), 'Christopher Nolan')
|
||
u.fullname = None
|
||
u.save()
|
||
self.assertEqual(u.get_full_name(), 'test@foo.bar')
|
||
self.assertEqual(u.get_short_name(), 'test@foo.bar')
|
||
|
||
|
||
class BaseQuotaTestCase(TestCase):
|
||
|
||
def setUp(self):
|
||
self.o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
self.event = Event.objects.create(
|
||
organizer=self.o, name='Dummy', slug='dummy',
|
||
date_from=now(), plugins='tests.testdummy'
|
||
)
|
||
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, personalized=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')
|
||
|
||
|
||
@pytest.mark.django_db(transaction=True)
|
||
@scopes_disabled()
|
||
def test_verify_repeatable_read_check():
|
||
if 'sqlite' in settings.DATABASES['default']['ENGINE']:
|
||
pytest.skip('Not supported on SQLite')
|
||
|
||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
event = Event.objects.create(
|
||
organizer=o, name='Dummy', slug='dummy',
|
||
date_from=now(), plugins='tests.testdummy'
|
||
)
|
||
quota = Quota.objects.create(name="Test", size=2, event=event)
|
||
|
||
with repeatable_reads_transaction():
|
||
with pytest.raises(ValueError):
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(quota)
|
||
qa.compute()
|
||
qa = QuotaAvailability(full_results=True, allow_repeatable_read=True)
|
||
qa.queue(quota)
|
||
qa.compute()
|
||
|
||
|
||
@pytest.mark.usefixtures("fakeredis_client")
|
||
class QuotaTestCase(BaseQuotaTestCase):
|
||
@classscope(attr='o')
|
||
def test_available(self):
|
||
self.quota.items.add(self.item1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
self.quota.items.add(self.item2)
|
||
self.quota.variations.add(self.var1)
|
||
try:
|
||
self.item2.check_quotas()
|
||
self.assertTrue(False)
|
||
except:
|
||
pass
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
|
||
@classscope(attr='o')
|
||
def test_sold_out(self):
|
||
self.quota.items.add(self.item1)
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
|
||
|
||
self.quota.items.add(self.item2)
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 3
|
||
self.quota.save()
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item2, variation=self.var1, price=2)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_ordered(self):
|
||
self.quota.items.add(self.item1)
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
order.expires = now() - timedelta(days=3)
|
||
order.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
order.status = Order.STATUS_EXPIRED
|
||
order.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_ordered_multi_quota(self):
|
||
quota2 = Quota.objects.create(name="Test", size=2, event=self.event)
|
||
quota2.items.add(self.item2)
|
||
quota2.variations.add(self.var1)
|
||
self.quota.items.add(self.item2)
|
||
self.quota.variations.add(self.var1)
|
||
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item2, variation=self.var1, price=2)
|
||
|
||
self.assertEqual(quota2.availability(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_position_canceled(self):
|
||
self.quota.items.add(self.item1)
|
||
self.quota.size = 3
|
||
self.quota.save()
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
op = OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
op.canceled = True
|
||
op.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 3))
|
||
|
||
@classscope(attr='o')
|
||
def test_reserved(self):
|
||
self.quota.items.add(self.item1)
|
||
self.quota.size = 3
|
||
self.quota.save()
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=4)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
cp = CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||
expires=now() + timedelta(days=3))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
|
||
|
||
cp.expires = now() - timedelta(days=3)
|
||
cp.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
self.quota.items.add(self.item2)
|
||
self.quota.variations.add(self.var1)
|
||
cp = CartPosition.objects.create(event=self.event, item=self.item2, variation=self.var1,
|
||
price=2, expires=now() + timedelta(days=3))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_multiple(self):
|
||
self.quota.items.add(self.item1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
|
||
quota2 = Quota.objects.create(event=self.event, name="Test 2", size=1)
|
||
quota2.items.add(self.item1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
quota2.size = 0
|
||
quota2.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_ignore_quotas(self):
|
||
self.quota.items.add(self.item1)
|
||
quota2 = Quota.objects.create(event=self.event, name="Test 2", size=0)
|
||
quota2.items.add(self.item1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
|
||
self.assertEqual(self.item1.check_quotas(ignored_quotas=[quota2]), (Quota.AVAILABILITY_OK, 2))
|
||
self.assertEqual(self.item1.check_quotas(ignored_quotas=[self.quota, quota2]),
|
||
(Quota.AVAILABILITY_OK, sys.maxsize))
|
||
|
||
@classscope(attr='o')
|
||
def test_unlimited(self):
|
||
self.quota.items.add(self.item1)
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
|
||
|
||
self.quota.size = None
|
||
self.quota.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, None))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_product(self):
|
||
self.quota.items.add(self.item1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
|
||
v = Voucher.objects.create(item=self.item1, event=self.event)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertTrue(v.is_active())
|
||
|
||
v.block_quota = True
|
||
v.save()
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_variation(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
|
||
v = Voucher.objects.create(item=self.item2, variation=self.var1, event=self.event)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertTrue(v.is_active())
|
||
|
||
v.block_quota = True
|
||
v.save()
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_all_variations(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
|
||
self.quota2 = Quota.objects.create(name="Test", size=2, event=self.event)
|
||
self.quota2.variations.add(self.var2)
|
||
|
||
self.quota3 = Quota.objects.create(name="Test", size=2, event=self.event)
|
||
self.quota3.variations.add(self.var3)
|
||
|
||
v = Voucher.objects.create(item=self.item2, event=self.event)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertEqual(self.var2.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
self.assertEqual(self.var3.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
|
||
v.block_quota = True
|
||
v.save()
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
self.assertEqual(self.var2.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertEqual(self.var3.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_quota(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertTrue(v.is_active())
|
||
|
||
v.block_quota = True
|
||
v.save()
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_quota_multiuse(self):
|
||
self.quota.size = 5
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.save()
|
||
Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=5, redeemed=2)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||
Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_multiuse_count_overredeemed(self):
|
||
if 'sqlite' not in settings.DATABASES['default']['ENGINE']:
|
||
pytest.xfail('This should raise a type error on most databases')
|
||
Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2, redeemed=4)
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 0)
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_quota_multiuse_multiproduct(self):
|
||
q2 = Quota.objects.create(event=self.event, name="foo", size=10)
|
||
q2.items.add(self.item1)
|
||
self.quota.size = 5
|
||
self.quota.items.add(self.item1)
|
||
self.quota.items.add(self.item2)
|
||
self.quota.items.add(self.item3)
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.variations.add(self.var2)
|
||
self.quota.variations.add(self.var3)
|
||
self.quota.save()
|
||
Voucher.objects.create(item=self.item1, event=self.event, block_quota=True, max_usages=5, redeemed=2)
|
||
Voucher.objects.create(item=self.item2, variation=self.var2, event=self.event, block_quota=True, max_usages=5,
|
||
redeemed=2)
|
||
Voucher.objects.create(item=self.item2, variation=self.var2, event=self.event, block_quota=True, max_usages=5,
|
||
redeemed=2)
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 9)
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_quota_expiring_soon(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5),
|
||
block_quota=True)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_quota_expired(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.size = 1
|
||
self.quota.save()
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() - timedelta(days=5),
|
||
block_quota=True)
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertFalse(v.is_active())
|
||
|
||
@classscope(attr='o')
|
||
def test_blocking_voucher_in_cart(self):
|
||
self.quota.items.add(self.item1)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5),
|
||
block_quota=True)
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
self.assertTrue(v.is_in_cart())
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 1)
|
||
self.assertEqual(qa.count_cart[self.quota], 0)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_blocking_voucher_in_cart_inifinitely_valid(self):
|
||
self.quota.items.add(self.item1)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True)
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 1)
|
||
self.assertEqual(qa.count_cart[self.quota], 0)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_blocking_expired_voucher_in_cart(self):
|
||
self.quota.items.add(self.item1)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() - timedelta(days=5),
|
||
block_quota=True)
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 0)
|
||
self.assertEqual(qa.count_cart[self.quota], 1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_nonblocking_voucher_in_cart(self):
|
||
self.quota.items.add(self.item1)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
qa = QuotaAvailability(full_results=True)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(qa.count_vouchers[self.quota], 0)
|
||
self.assertEqual(qa.count_cart[self.quota], 1)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_waitinglist_auto_disable(self):
|
||
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
|
||
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=True)
|
||
)
|
||
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_ORDERED, 0))
|
||
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
|
||
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
|
||
)
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
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_ORDERED, 0))
|
||
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
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_ORDERED, 0))
|
||
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
def test_waitinglist_cache_separation(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'
|
||
)
|
||
|
||
# Check that there is no "cache mixup" even across multiple runs
|
||
qa = QuotaAvailability(count_waitinglist=False)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
qa = QuotaAvailability(count_waitinglist=True)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||
|
||
qa = QuotaAvailability(count_waitinglist=True)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||
|
||
qa = QuotaAvailability(count_waitinglist=False)
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
# Rebuild cache required
|
||
self.quota.size = 5
|
||
self.quota.save()
|
||
|
||
qa = QuotaAvailability(count_waitinglist=True)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_ORDERED, 0)
|
||
|
||
qa = QuotaAvailability(count_waitinglist=False)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
self.quota.rebuild_cache()
|
||
|
||
qa = QuotaAvailability(count_waitinglist=True)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 4)
|
||
|
||
qa = QuotaAvailability(count_waitinglist=False)
|
||
qa.queue(self.quota)
|
||
qa.compute(allow_cache=True)
|
||
assert qa.results[self.quota] == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
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
|
||
)
|
||
qa = QuotaAvailability()
|
||
qa.queue(self.quota)
|
||
qa.compute()
|
||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
|
||
|
||
@classscope(attr='o')
|
||
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))
|
||
|
||
@classscope(attr='o')
|
||
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_ORDERED, 0))
|
||
|
||
with self.assertNumQueries(1):
|
||
self.assertEqual(self.var1.check_quotas(_cache=cache), (Quota.AVAILABILITY_ORDERED, 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))
|
||
|
||
@classscope(attr='o')
|
||
def test_ignore_if_blocked(self):
|
||
q1 = self.event.quotas.create(name="Q1", size=50)
|
||
q1.items.add(self.item1)
|
||
|
||
# Create orders
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
expires=now() + timedelta(days=3),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
total=6)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2, blocked=["foo"])
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2, blocked=["foo"], ignore_from_quota_while_blocked=True)
|
||
OrderPosition.objects.create(order=order, item=self.item1, price=2, ignore_from_quota_while_blocked=True)
|
||
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 47))
|
||
|
||
@classscope(attr='o')
|
||
def test_subevent_isolation(self):
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||
se2 = self.event.subevents.create(date_from=now(), name="SE 2")
|
||
q1 = self.event.quotas.create(name="Q1", subevent=se1, size=50)
|
||
q2 = self.event.quotas.create(name="Q2", subevent=se2, size=50)
|
||
q1.items.add(self.item1)
|
||
q2.items.add(self.item1)
|
||
|
||
# Create orders
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
expires=now() + timedelta(days=3),
|
||
total=6)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se2, price=2)
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
expires=now() + timedelta(days=3),
|
||
total=8)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se2, price=2)
|
||
|
||
Voucher.objects.create(item=self.item1, event=self.event, valid_until=now() + timedelta(days=5),
|
||
block_quota=True, max_usages=6, subevent=se1)
|
||
Voucher.objects.create(item=self.item1, event=self.event, valid_until=now() + timedelta(days=5),
|
||
block_quota=True, max_usages=4, subevent=se2)
|
||
|
||
for i in range(8):
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2, subevent=se1,
|
||
expires=now() + timedelta(days=3))
|
||
|
||
for i in range(5):
|
||
CartPosition.objects.create(event=self.event, item=self.item1, price=2, subevent=se2,
|
||
expires=now() + timedelta(days=3))
|
||
|
||
for i in range(16):
|
||
WaitingListEntry.objects.create(
|
||
event=self.event, item=self.item1, email='foo@bar.com', subevent=se1
|
||
)
|
||
|
||
for i in range(13):
|
||
WaitingListEntry.objects.create(
|
||
event=self.event, item=self.item1, email='foo@bar.com', subevent=se2
|
||
)
|
||
|
||
with self.assertRaises(TypeError):
|
||
self.item1.check_quotas()
|
||
|
||
self.assertEqual(self.item1.check_quotas(subevent=se1), (Quota.AVAILABILITY_OK, 50 - 5 - 6 - 8 - 16))
|
||
self.assertEqual(self.item1.check_quotas(subevent=se2), (Quota.AVAILABILITY_OK, 50 - 2 - 4 - 5 - 13))
|
||
self.assertEqual(q1.availability(), (Quota.AVAILABILITY_OK, 50 - 5 - 6 - 8 - 16))
|
||
self.assertEqual(q2.availability(), (Quota.AVAILABILITY_OK, 50 - 2 - 4 - 5 - 13))
|
||
self.event.has_subevents = False
|
||
self.event.save()
|
||
|
||
@classscope(attr='o')
|
||
def test_close_when_full_on_calculation(self):
|
||
self.quota.close_when_sold_out = True
|
||
self.quota.size = 0
|
||
self.quota.save()
|
||
assert not self.quota.closed
|
||
self.quota.availability()
|
||
self.quota.refresh_from_db()
|
||
assert self.quota.closed
|
||
assert self.quota.all_logentries().filter(action_type="pretix.event.quota.closed").exists()
|
||
|
||
@classscope(attr='o')
|
||
def test_closed_reports_as_sold_out(self):
|
||
self.quota.closed = True
|
||
self.quota.size = 100
|
||
self.quota.save()
|
||
assert self.quota.availability() == (Quota.AVAILABILITY_ORDERED, 0)
|
||
|
||
|
||
class CheckinQuotaTestCase(BaseQuotaTestCase):
|
||
|
||
@scopes_disabled()
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.quota.size = 5
|
||
self.quota.release_after_exit = True
|
||
self.quota.save()
|
||
self.quota.items.add(self.item1)
|
||
self.cl = self.event.checkin_lists.create(name="Test", allow_entry_after_exit=False)
|
||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
expires=now() + timedelta(days=3),
|
||
total=4)
|
||
self.op = OrderPosition.objects.create(order=order, item=self.item1, price=2)
|
||
|
||
@classscope(attr='o')
|
||
def test_not_checked_in(self):
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
@classscope(attr='o')
|
||
def test_checked_in(self):
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_ENTRY, datetime=now() - timedelta(minutes=5))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
@classscope(attr='o')
|
||
def test_checked_in_and_out(self):
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_ENTRY, datetime=now() - timedelta(minutes=5))
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_EXIT, datetime=now() - timedelta(minutes=2))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 5))
|
||
|
||
@classscope(attr='o')
|
||
def test_wrong_order(self):
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_ENTRY, datetime=now() - timedelta(minutes=2))
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_EXIT, datetime=now() - timedelta(minutes=5))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
@classscope(attr='o')
|
||
def test_allows_reentry(self):
|
||
self.cl.allow_entry_after_exit = True
|
||
self.cl.save()
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_ENTRY, datetime=now() - timedelta(minutes=5))
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_EXIT, datetime=now() - timedelta(minutes=2))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
@classscope(attr='o')
|
||
def test_feature_disabled(self):
|
||
self.quota.release_after_exit = False
|
||
self.quota.save()
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_ENTRY, datetime=now() - timedelta(minutes=5))
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_EXIT, datetime=now() - timedelta(minutes=2))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
@classscope(attr='o')
|
||
def test_checked_out(self):
|
||
self.op.checkins.create(list=self.cl, type=Checkin.TYPE_EXIT, datetime=now() - timedelta(minutes=5))
|
||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 4))
|
||
|
||
|
||
class BundleQuotaTestCase(BaseQuotaTestCase):
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.quota.size = 5
|
||
self.quota.save()
|
||
self.trans = Item.objects.create(event=self.event, name='Public Transport Ticket',
|
||
default_price=2.50)
|
||
self.transquota = Quota.objects.create(event=self.event, name='Transport', size=10)
|
||
self.transquota.items.add(self.trans)
|
||
self.quota.items.add(self.item1)
|
||
self.quota.items.add(self.item2)
|
||
self.quota.variations.add(self.var1)
|
||
self.bundle1 = ItemBundle.objects.create(
|
||
base_item=self.item1,
|
||
bundled_item=self.trans,
|
||
designated_price=1.5,
|
||
count=1
|
||
)
|
||
self.bundle2 = ItemBundle.objects.create(
|
||
base_item=self.item2,
|
||
bundled_item=self.trans,
|
||
designated_price=1.5,
|
||
count=1
|
||
)
|
||
|
||
@classscope(attr='o')
|
||
def test_only_respect_with_flag(self):
|
||
assert self.item1.check_quotas() == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
def test_do_not_exceed(self):
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
def test_limited_by_bundled_quita(self):
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 3)
|
||
|
||
@classscope(attr='o')
|
||
def test_multiple_bundles(self):
|
||
ItemBundle.objects.create(
|
||
base_item=self.item1,
|
||
bundled_item=self.trans,
|
||
designated_price=1.5,
|
||
count=1
|
||
)
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
@classscope(attr='o')
|
||
def test_bundle_count(self):
|
||
self.bundle1.count = 2
|
||
self.bundle1.save()
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
@classscope(attr='o')
|
||
def test_bundled_unlimited(self):
|
||
self.transquota.size = None
|
||
self.transquota.save()
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 5)
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
def test_item_unlimited(self):
|
||
self.quota.size = None
|
||
self.quota.save()
|
||
assert self.item1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 10)
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 10)
|
||
|
||
@classscope(attr='o')
|
||
def test_var_only_respect_with_flag(self):
|
||
assert self.var1.check_quotas() == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
def test_var_do_not_exceed(self):
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 5)
|
||
|
||
@classscope(attr='o')
|
||
def test_var_limited_by_bundled_quita(self):
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 3)
|
||
|
||
@classscope(attr='o')
|
||
def test_var_multiple_bundles(self):
|
||
ItemBundle.objects.create(
|
||
base_item=self.item2,
|
||
bundled_item=self.trans,
|
||
designated_price=1.5,
|
||
count=1
|
||
)
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
@classscope(attr='o')
|
||
def test_var_bundle_count(self):
|
||
self.bundle2.count = 2
|
||
self.bundle2.save()
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 1)
|
||
|
||
@classscope(attr='o')
|
||
def test_bundled_variation(self):
|
||
v = self.trans.variations.create(value="foo", default_price=4)
|
||
self.transquota.variations.add(v)
|
||
self.bundle2.bundled_variation = v
|
||
self.bundle2.save()
|
||
self.transquota.size = 3
|
||
self.transquota.save()
|
||
assert self.var1.check_quotas(include_bundled=True) == (Quota.AVAILABILITY_OK, 3)
|
||
|
||
|
||
class WaitingListTestCase(BaseQuotaTestCase):
|
||
|
||
@classscope(attr='o')
|
||
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()
|
||
|
||
@classscope(attr='o')
|
||
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()
|
||
|
||
@classscope(attr='o')
|
||
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):
|
||
|
||
@classscope(attr='o')
|
||
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))
|
||
self.assertTrue(v.is_active())
|
||
self.assertFalse(v.is_in_cart())
|
||
self.assertFalse(v.is_ordered())
|
||
|
||
# use a voucher normally
|
||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
self.assertTrue(v.is_active())
|
||
self.assertTrue(v.is_in_cart())
|
||
self.assertFalse(v.is_ordered())
|
||
|
||
order = perform_order(
|
||
event=self.event.id,
|
||
positions=[cart.id],
|
||
payments=[{
|
||
"id": "test0",
|
||
"provider": "free",
|
||
"max_value": None,
|
||
"min_value": None,
|
||
"multi_use_supported": False,
|
||
"info_data": {},
|
||
}],
|
||
)
|
||
v.refresh_from_db()
|
||
self.assertFalse(v.is_active())
|
||
self.assertFalse(v.is_in_cart())
|
||
self.assertTrue(v.is_ordered())
|
||
|
||
# assert that the voucher cannot be reused
|
||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
self.assertRaises(
|
||
OrderError,
|
||
perform_order,
|
||
event=self.event.id,
|
||
positions=[cart.id],
|
||
payments=[{
|
||
"id": "test0",
|
||
"provider": "free",
|
||
"max_value": None,
|
||
"min_value": None,
|
||
"multi_use_supported": False,
|
||
"info_data": {},
|
||
}],
|
||
)
|
||
|
||
# assert that the voucher can be re-used after cancelling the successful order
|
||
cancel_order(order['order_id'])
|
||
v.refresh_from_db()
|
||
self.assertTrue(v.is_active())
|
||
self.assertFalse(v.is_in_cart())
|
||
self.assertTrue(v.is_ordered())
|
||
|
||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||
expires=now() + timedelta(days=3), voucher=v)
|
||
perform_order(
|
||
event=self.event.id,
|
||
positions=[cart.id],
|
||
payments=[{
|
||
"id": "test0",
|
||
"provider": "free",
|
||
"max_value": None,
|
||
"min_value": None,
|
||
"multi_use_supported": False,
|
||
"info_data": {},
|
||
}],
|
||
)
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_applicability_quota(self):
|
||
self.quota.items.add(self.item1)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||
self.assertTrue(v.applies_to(self.item1))
|
||
self.assertFalse(v.applies_to(self.item2))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_applicability_item(self):
|
||
v = Voucher.objects.create(item=self.var1.item, event=self.event)
|
||
self.assertFalse(v.applies_to(self.item1))
|
||
self.assertTrue(v.applies_to(self.var1.item))
|
||
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_applicability_variation(self):
|
||
v = Voucher.objects.create(item=self.var1.item, variation=self.var1, event=self.event)
|
||
self.assertFalse(v.applies_to(self.item1))
|
||
self.assertFalse(v.applies_to(self.var1.item))
|
||
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||
self.assertFalse(v.applies_to(self.var1.item, self.var2))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_applicability_all(self):
|
||
v = Voucher.objects.create(event=self.event)
|
||
self.assertTrue(v.applies_to(self.item1))
|
||
self.assertTrue(v.applies_to(self.var1.item))
|
||
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||
self.assertTrue(v.applies_to(self.var1.item, self.var2))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_applicability_variation_through_quota(self):
|
||
self.quota.variations.add(self.var1)
|
||
self.quota.items.add(self.var1.item)
|
||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||
self.assertFalse(v.applies_to(self.item1))
|
||
self.assertTrue(v.applies_to(self.var1.item)) # semantics unclear
|
||
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||
self.assertFalse(v.applies_to(self.var1.item, self.var2))
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_no_item_with_quota(self):
|
||
with self.assertRaises(ValidationError):
|
||
v = Voucher(quota=self.quota, item=self.item1, event=self.event)
|
||
v.clean()
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_item_with_no_variation(self):
|
||
with self.assertRaises(ValidationError):
|
||
v = Voucher(item=self.item1, variation=self.var1, event=self.event)
|
||
v.clean()
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_item_does_not_match_variation(self):
|
||
with self.assertRaises(ValidationError):
|
||
v = Voucher(item=self.item2, variation=self.var3, event=self.event)
|
||
v.clean()
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_specify_variation_for_block_quota(self):
|
||
v = Voucher(item=self.item2, block_quota=True, event=self.event)
|
||
v.clean()
|
||
|
||
@classscope(attr='o')
|
||
def test_voucher_no_item_but_variation(self):
|
||
with self.assertRaises(ValidationError):
|
||
v = Voucher(variation=self.var1, event=self.event)
|
||
v.clean()
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_none(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='none', value=Decimal('10.00'))
|
||
assert v.calculate_price(Decimal('23.42')) == Decimal('23.42')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_set_empty(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='set')
|
||
assert v.calculate_price(Decimal('23.42')) == Decimal('23.42')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_set(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='set', value=Decimal('10.00'))
|
||
assert v.calculate_price(Decimal('23.42')) == Decimal('10.00')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_set_zero(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='set', value=Decimal('0.00'))
|
||
assert v.calculate_price(Decimal('23.42')) == Decimal('0.00')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_subtract(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
|
||
assert v.calculate_price(Decimal('23.42')) == Decimal('13.42')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_percent(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='percent', value=Decimal('23.00'))
|
||
assert v.calculate_price(Decimal('100.00')) == Decimal('77.00')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_price_max_discount(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
|
||
assert v.calculate_price(Decimal('23.42'), max_discount=Decimal('5.00')) == Decimal('18.42')
|
||
|
||
@classscope(attr='o')
|
||
def test_calculate_budget_used(self):
|
||
v = Voucher.objects.create(event=self.event, price_mode='sset', value=Decimal('20.00'))
|
||
|
||
order = Order.objects.create(
|
||
status=Order.STATUS_PENDING, event=self.event,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
|
||
voucher_budget_use=Decimal('3.00')
|
||
)
|
||
assert v.budget_used() == Decimal('3.00')
|
||
|
||
order = Order.objects.create(
|
||
status=Order.STATUS_PAID, event=self.event,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
|
||
)
|
||
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
|
||
voucher_budget_use=Decimal('3.00'))
|
||
assert v.budget_used() == Decimal('6.00')
|
||
|
||
|
||
class OrderTestCase(BaseQuotaTestCase):
|
||
def setUp(self):
|
||
super().setUp()
|
||
with scope(organizer=self.o):
|
||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||
self.order = Order.objects.create(
|
||
status=Order.STATUS_PENDING, event=self.event,
|
||
datetime=now() - timedelta(days=5),
|
||
expires=now() + timedelta(days=5), total=46,
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
)
|
||
self.quota.items.add(self.item1)
|
||
self.op1 = OrderPosition.objects.create(order=self.order, item=self.item1,
|
||
variation=None, price=23)
|
||
self.op2 = OrderPosition.objects.create(order=self.order, item=self.item1,
|
||
variation=None, price=23)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_in_time(self):
|
||
self.quota.size = 0
|
||
self.quota.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
assert not self.order.all_logentries().filter(action_type='pretix.event.order.overpaid').exists()
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_available(self):
|
||
self.event.settings.payment_term_last = (now() + timedelta(days=2)).strftime('%Y-%m-%d')
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_after_last_date(self):
|
||
self.event.settings.payment_term_last = (now() - timedelta(days=2)).strftime('%Y-%m-%d')
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.save()
|
||
with self.assertRaises(Quota.QuotaExceededException):
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_after_last_date_subevent_relative(self):
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
self.event.settings.set('payment_term_last', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.save()
|
||
with self.assertRaises(Quota.QuotaExceededException):
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
|
||
self.event.has_subevents = False
|
||
self.event.save()
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_late_not_allowed(self):
|
||
self.event.settings.payment_term_accept_late = False
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.save()
|
||
with self.assertRaises(Quota.QuotaExceededException):
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_unavailable(self):
|
||
self.event.settings.payment_term_accept_late = True
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.save()
|
||
self.quota.size = 0
|
||
self.quota.save()
|
||
with self.assertRaises(Quota.QuotaExceededException):
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertIn(self.order.status, (Order.STATUS_PENDING, Order.STATUS_EXPIRED))
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_after_deadline_but_not_expired(self):
|
||
self.event.settings.payment_term_accept_late = True
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_unavailable_force(self):
|
||
self.event.settings.payment_term_accept_late = True
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.save()
|
||
self.quota.size = 0
|
||
self.quota.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm(force=True)
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_unavailable_waiting_list(self):
|
||
self.event.settings.payment_term_accept_late = True
|
||
self.event.waitinglistentries.create(item=self.item1, email='foo@bar.com')
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.save()
|
||
self.quota.size = 2
|
||
self.quota.save()
|
||
with self.assertRaises(Quota.QuotaExceededException):
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm()
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_expired_unavailable_waiting_list_ignore(self):
|
||
self.event.waitinglistentries.create(item=self.item1, email='foo@bar.com')
|
||
self.order.expires = now() - timedelta(days=2)
|
||
self.order.status = Order.STATUS_EXPIRED
|
||
self.order.save()
|
||
self.quota.size = 2
|
||
self.quota.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total
|
||
).confirm(count_waitinglist=False)
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_overpaid(self):
|
||
self.quota.size = 2
|
||
self.quota.save()
|
||
self.order.payments.create(
|
||
provider='manual', amount=self.order.total + 2
|
||
).confirm(count_waitinglist=False)
|
||
self.order = Order.objects.get(id=self.order.id)
|
||
self.assertEqual(self.order.status, Order.STATUS_PAID)
|
||
assert self.order.all_logentries().filter(action_type='pretix.event.order.overpaid').exists()
|
||
|
||
@classscope(attr='o')
|
||
def test_can_modify_answers(self):
|
||
self.event.settings.set('invoice_address_asked', False)
|
||
self.event.settings.set('attendee_names_asked', True)
|
||
assert self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
|
||
self.event.settings.set('allow_modifications', 'attendee')
|
||
assert self.op1.can_modify_answers
|
||
|
||
self.event.settings.set('attendee_names_asked', False)
|
||
assert not self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
self.event.settings.set('invoice_address_asked', True)
|
||
assert self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
self.event.settings.set('invoice_address_asked', False)
|
||
self.event.settings.set('invoice_name_required', True)
|
||
assert self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
q = Question.objects.create(question='Foo', type=Question.TYPE_BOOLEAN, event=self.event)
|
||
self.item1.questions.add(q)
|
||
assert self.order.can_modify_answers
|
||
assert self.op1.can_modify_answers
|
||
self.order.status = Order.STATUS_CANCELED
|
||
assert not self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
self.order.status = Order.STATUS_PAID
|
||
assert self.order.can_modify_answers
|
||
assert self.op1.can_modify_answers
|
||
self.event.settings.set('last_order_modification_date', now() - timedelta(days=1))
|
||
assert not self.order.can_modify_answers
|
||
assert not self.op1.can_modify_answers
|
||
|
||
@classscope(attr='o')
|
||
def test_can_modify_answers_subevent(self):
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=8))
|
||
se3 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
self.event.settings.set('last_order_modification_date', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
assert self.order.can_modify_answers
|
||
self.op2.subevent = se3
|
||
self.op2.save()
|
||
assert not self.order.can_modify_answers
|
||
self.event.has_subevents = False
|
||
self.event.save()
|
||
|
||
@classscope(attr='o')
|
||
def test_payment_term_last_relative(self):
|
||
self.event.settings.set('payment_term_last', date(2017, 5, 3))
|
||
assert self.order.payment_term_last == datetime.datetime(2017, 5, 3, 23, 59, 59, tzinfo=datetime.timezone.utc)
|
||
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||
self.event.save()
|
||
self.event.settings.set('payment_term_last', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
assert self.order.payment_term_last == datetime.datetime(2017, 5, 1, 23, 59, 59, tzinfo=datetime.timezone.utc)
|
||
|
||
@classscope(attr='o')
|
||
def test_payment_term_last_subevent(self):
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=8))
|
||
se3 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
self.event.settings.set('payment_term_last', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
assert self.order.payment_term_last > now()
|
||
self.op2.subevent = se3
|
||
self.op2.save()
|
||
assert self.order.payment_term_last < now()
|
||
self.event.has_subevents = False
|
||
self.event.save()
|
||
|
||
@classscope(attr='o')
|
||
def test_ticket_download_date_relative(self):
|
||
self.event.settings.set('ticket_download_date', datetime.datetime(2017, 5, 3, 12, 59, 59, tzinfo=datetime.timezone.utc))
|
||
assert self.order.ticket_download_date == datetime.datetime(2017, 5, 3, 12, 59, 59, tzinfo=datetime.timezone.utc)
|
||
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||
self.event.save()
|
||
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
assert self.order.ticket_download_date == datetime.datetime(2017, 5, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||
|
||
@classscope(attr='o')
|
||
def test_ticket_download_date_subevent(self):
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=8))
|
||
se3 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
|
||
))
|
||
assert self.order.ticket_download_date > now()
|
||
self.op2.subevent = se3
|
||
self.op2.save()
|
||
assert self.order.ticket_download_date < now()
|
||
self.event.has_subevents = False
|
||
self.event.save()
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
assert self.order.user_cancel_allowed
|
||
self.event.settings.cancel_allow_user = False
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order_with_giftcard(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True, issue_giftcard=True)
|
||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
self.event.organizer.issued_gift_cards.create(
|
||
currency="EUR", issued_in=p
|
||
)
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order_with_membership(self):
|
||
mt = self.event.organizer.membership_types.create(name="foo")
|
||
customer = self.event.organizer.customers.create()
|
||
self.order.customer = customer
|
||
self.order.save()
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True, issue_giftcard=True)
|
||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
m = customer.memberships.create(
|
||
membership_type=mt,
|
||
date_start=now(),
|
||
date_end=now(),
|
||
granted_in=p,
|
||
)
|
||
# yeah, doesn't really make sense on same order, but good enough for the test
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23, used_membership=m)
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order_free(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.total = Decimal('0.00')
|
||
self.order.save()
|
||
assert self.order.user_cancel_allowed
|
||
self.event.settings.cancel_allow_user = False
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order_paid(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
assert not self.order.user_cancel_allowed
|
||
self.event.settings.cancel_allow_user = False
|
||
self.event.settings.cancel_allow_user_paid = True
|
||
assert self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_cancel_checked_in(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.event.settings.cancel_allow_user = False
|
||
self.event.settings.cancel_allow_user_paid = True
|
||
assert self.order.user_cancel_allowed
|
||
Checkin.objects.create(
|
||
position=self.order.positions.first(),
|
||
list=CheckinList.objects.create(event=self.event, name='Default')
|
||
)
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_cancel_blocked(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.event.settings.cancel_allow_user = False
|
||
self.event.settings.cancel_allow_user_paid = True
|
||
assert self.order.user_cancel_allowed
|
||
self.op1.blocked = ["admin"]
|
||
self.op1.save()
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_cancel_order_multiple(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=None, price=23)
|
||
assert self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_cancel_order(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
assert self.order.user_cancel_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_cancel_order_multiple(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=None, price=23)
|
||
assert self.order.user_cancel_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_cancel_order_multiple_mixed(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=None, price=23)
|
||
assert self.order.user_cancel_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_no_duplicate_position_secret(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
p1 = OrderPosition.objects.create(order=self.order, item=item1, secret='ABC',
|
||
variation=None, price=23)
|
||
p2 = OrderPosition.objects.create(order=self.order, item=item1, secret='ABC',
|
||
variation=None, price=23)
|
||
assert p1.secret != p2.secret
|
||
assert self.order.user_cancel_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_user_cancel_absolute_deadline_unpaid_no_subevents(self):
|
||
assert self.order.user_cancel_deadline is None
|
||
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
|
||
now() + timedelta(days=1)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline > now()
|
||
assert self.order.user_cancel_allowed
|
||
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
|
||
now() - timedelta(days=1)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline < now()
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_user_cancel_relative_deadline_unpaid_no_subevents(self):
|
||
self.event.date_from = now() + timedelta(days=3)
|
||
self.event.save()
|
||
|
||
assert self.order.user_cancel_deadline is None
|
||
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline > now()
|
||
assert self.order.user_cancel_allowed
|
||
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline < now()
|
||
assert not self.order.user_cancel_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_user_cancel_relative_deadline_to_subevents(self):
|
||
self.event.date_from = now() + timedelta(days=3)
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
|
||
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline < now()
|
||
self.op2.subevent = se1
|
||
self.op2.save()
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_deadline > now()
|
||
|
||
@classscope(attr='o')
|
||
def test_user_cancel_fee(self):
|
||
self.order.fees.create(fee_type=OrderFee.FEE_TYPE_SHIPPING, value=Decimal('2.00'))
|
||
self.order.total = 48
|
||
self.order.save()
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
assert self.order.user_cancel_fee == Decimal('0.00')
|
||
|
||
self.event.settings.cancel_allow_user_paid_keep = Decimal('2.50')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('2.50')
|
||
|
||
self.event.settings.cancel_allow_user_paid_keep_percentage = Decimal('10.0')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('7.30')
|
||
|
||
self.event.settings.cancel_allow_user_paid_keep_fees = True
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('9.10')
|
||
|
||
self.event.settings.cancel_allow_user_paid_keep = Decimal('100.00')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('48.00')
|
||
|
||
self.order.status = Order.STATUS_PENDING
|
||
self.order.save()
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('0.00')
|
||
|
||
self.event.settings.cancel_allow_user_unpaid_keep = Decimal('2.50')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('2.50')
|
||
|
||
self.event.settings.cancel_allow_user_unpaid_keep_percentage = Decimal('5.0')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('4.90')
|
||
|
||
self.event.settings.cancel_allow_user_unpaid_keep_fees = True
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('6.80')
|
||
|
||
self.event.settings.cancel_allow_user_unpaid_keep = Decimal('100.00')
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_cancel_fee == Decimal('48.00')
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_order_underpaid(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_DONE,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('10.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert o.is_underpaid
|
||
assert not o.is_overpaid
|
||
assert not o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_pending_order_underpaid(self):
|
||
self.order.payments.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_DONE,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('10.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert not o.is_overpaid
|
||
assert not o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_canceled_order_overpaid(self):
|
||
self.order.status = Order.STATUS_CANCELED
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_DONE,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('-36.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert o.is_overpaid
|
||
assert not o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_order_external_refund(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_EXTERNAL,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('0.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert not o.is_overpaid
|
||
assert not o.has_pending_refund
|
||
assert o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_pending_order_pending_refund(self):
|
||
self.order.status = Order.STATUS_CANCELED
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('46.00'),
|
||
state=OrderRefund.REFUND_STATE_CREATED,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('0.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert not o.is_overpaid
|
||
assert o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_paid_order_overpaid(self):
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('66.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_DONE,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('-10.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert o.is_overpaid
|
||
assert not o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_pending_order_overpaid(self):
|
||
self.order.status = Order.STATUS_PENDING
|
||
self.order.save()
|
||
self.order.payments.create(
|
||
amount=Decimal('56.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='manual'
|
||
)
|
||
self.order.refunds.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderRefund.REFUND_STATE_DONE,
|
||
provider='manual'
|
||
)
|
||
assert self.order.pending_sum == Decimal('0.00')
|
||
o = Order.annotate_overpayments(Order.objects.all()).first()
|
||
assert not o.is_underpaid
|
||
assert not o.is_overpaid
|
||
assert o.is_pending_with_full_payment
|
||
assert not o.has_pending_refund
|
||
assert not o.has_external_refund
|
||
|
||
@classscope(attr='o')
|
||
def test_canceled_positions(self):
|
||
self.op1.canceled = True
|
||
self.op1.save()
|
||
assert OrderPosition.objects.count() == 1
|
||
assert OrderPosition.all.count() == 2
|
||
assert self.order.positions.count() == 1
|
||
assert self.order.all_positions.count() == 2
|
||
|
||
@classscope(attr='o')
|
||
def test_propose_auto_refunds(self):
|
||
p1 = self.order.payments.create(
|
||
amount=Decimal('23.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='testdummy_fullrefund'
|
||
)
|
||
p2 = self.order.payments.create(
|
||
amount=Decimal('10.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='testdummy_partialrefund'
|
||
)
|
||
self.order.payments.create(
|
||
amount=Decimal('13.00'),
|
||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||
provider='testdummy'
|
||
)
|
||
assert self.order.propose_auto_refunds(Decimal('23.00')) == {
|
||
p1: Decimal('23.00')
|
||
}
|
||
assert self.order.propose_auto_refunds(Decimal('10.00')) == {
|
||
p2: Decimal('10.00')
|
||
}
|
||
assert self.order.propose_auto_refunds(Decimal('5.00')) == {
|
||
p2: Decimal('5.00')
|
||
}
|
||
assert self.order.propose_auto_refunds(Decimal('20.00')) == {
|
||
p2: Decimal('10.00')
|
||
}
|
||
assert self.order.propose_auto_refunds(Decimal('25.00')) == {
|
||
p1: Decimal('23.00'),
|
||
p2: Decimal('2.00'),
|
||
}
|
||
assert self.order.propose_auto_refunds(Decimal('35.00')) == {
|
||
p1: Decimal('23.00'),
|
||
p2: Decimal('10.00'),
|
||
}
|
||
|
||
@classscope(attr='o')
|
||
def test_can_change_order(self):
|
||
self.event.settings.change_allow_attendee = True
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v = item1.variations.create(value="V")
|
||
op = OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
assert not self.order.user_change_allowed
|
||
assert not op.attendee_change_allowed
|
||
self.event.settings.change_allow_user_variation = True
|
||
assert self.order.user_change_allowed
|
||
assert op.attendee_change_allowed
|
||
|
||
self.event.settings.change_allow_attendee = False
|
||
assert not op.attendee_change_allowed
|
||
self.event.settings.change_allow_attendee = True
|
||
|
||
self.event.settings.change_allow_user_variation = False
|
||
self.order.require_approval = True
|
||
assert not self.order.user_change_allowed
|
||
assert not op.attendee_change_allowed
|
||
self.event.settings.change_allow_user_variation = True
|
||
assert not self.order.user_change_allowed
|
||
assert not op.attendee_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_change_order_with_giftcard(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True, issue_giftcard=True)
|
||
v = item1.variations.create(value="V")
|
||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.settings.change_allow_attendee = True
|
||
self.event.organizer.issued_gift_cards.create(
|
||
currency="EUR", issued_in=p
|
||
)
|
||
assert not self.order.user_change_allowed
|
||
assert not p.attendee_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_change_checked_in(self):
|
||
v = self.item1.variations.create(value="V")
|
||
self.order.positions.update(variation=v)
|
||
self.order.status = Order.STATUS_PAID
|
||
self.order.save()
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.settings.change_allow_attendee = True
|
||
assert self.order.user_change_allowed
|
||
Checkin.objects.create(
|
||
position=self.order.positions.first(),
|
||
list=CheckinList.objects.create(event=self.event, name='Default')
|
||
)
|
||
assert not self.order.user_change_allowed
|
||
assert not self.order.positions.first().attendee_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_change_order_multiple(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v = item1.variations.create(value="V")
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v2 = item2.variations.create(value="V")
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=v2, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.settings.change_allow_attendee = True
|
||
assert self.order.user_change_allowed
|
||
assert not self.order.positions.first().attendee_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_change_order(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
v = item1.variations.create(value="V")
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.settings.change_allow_attendee = True
|
||
assert self.order.user_change_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_require_any_variation(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
p = OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=None, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.settings.change_allow_attendee = True
|
||
assert self.order.user_change_allowed is False
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v2 = item2.variations.create(value="V")
|
||
p2 = OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=v2, price=23)
|
||
assert self.order.user_change_allowed is True
|
||
assert p.attendee_change_allowed is False
|
||
assert p2.attendee_change_allowed is True
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_change_order_multiple(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v = item1.variations.create(value="V")
|
||
v2 = item2.variations.create(value="V")
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=v2, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
assert self.order.user_change_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_can_not_change_order_multiple_mixed(self):
|
||
item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=False)
|
||
item2 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||
admission=True, allow_cancel=True)
|
||
v = item1.variations.create(value="V")
|
||
v2 = item2.variations.create(value="V")
|
||
OrderPosition.objects.create(order=self.order, item=item1,
|
||
variation=v, price=23)
|
||
OrderPosition.objects.create(order=self.order, item=item2,
|
||
variation=v2, price=23)
|
||
self.event.settings.change_allow_user_variation = True
|
||
assert self.order.user_change_allowed is False
|
||
|
||
@classscope(attr='o')
|
||
def test_user_change_absolute_deadline_unpaid_no_subevents(self):
|
||
v = self.item1.variations.create(value="V")
|
||
self.order.positions.update(variation=v)
|
||
self.event.settings.change_allow_user_variation = True
|
||
assert self.order.user_change_deadline is None
|
||
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
|
||
now() + timedelta(days=1)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline > now()
|
||
assert self.order.user_change_allowed
|
||
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
|
||
now() - timedelta(days=1)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline < now()
|
||
assert not self.order.user_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_user_change_relative_deadline_unpaid_no_subevents(self):
|
||
v = self.item1.variations.create(value="V")
|
||
self.order.positions.update(variation=v)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.date_from = now() + timedelta(days=3)
|
||
self.event.save()
|
||
|
||
assert self.order.user_change_deadline is None
|
||
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline > now()
|
||
assert self.order.user_change_allowed
|
||
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline < now()
|
||
assert not self.order.user_change_allowed
|
||
|
||
@classscope(attr='o')
|
||
def test_user_change_relative_deadline_to_subevents(self):
|
||
v = self.item1.variations.create(value="V")
|
||
self.order.positions.update(variation=v)
|
||
self.event.settings.change_allow_user_variation = True
|
||
self.event.date_from = now() + timedelta(days=3)
|
||
self.event.has_subevents = True
|
||
self.event.save()
|
||
se1 = self.event.subevents.create(name="SE1", date_from=now() + timedelta(days=10))
|
||
se2 = self.event.subevents.create(name="SE2", date_from=now() + timedelta(days=1))
|
||
self.op1.subevent = se1
|
||
self.op1.save()
|
||
self.op2.subevent = se2
|
||
self.op2.save()
|
||
|
||
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
|
||
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
|
||
))
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline < now()
|
||
self.op2.subevent = se1
|
||
self.op2.save()
|
||
self.order = Order.objects.get(pk=self.order.pk)
|
||
assert self.order.user_change_deadline > now()
|
||
|
||
|
||
class ItemCategoryTest(TestCase):
|
||
"""
|
||
This test case tests various methods around the category model.
|
||
"""
|
||
|
||
def setUp(self):
|
||
self.o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
self.event = Event.objects.create(
|
||
organizer=self.o, name='Dummy', slug='dummy',
|
||
date_from=now(),
|
||
)
|
||
|
||
@classscope(attr='o')
|
||
def test_sorting(self):
|
||
c1 = ItemCategory.objects.create(event=self.event)
|
||
c2 = ItemCategory.objects.create(event=self.event)
|
||
assert c1 < c2
|
||
c1.position = 2
|
||
c2.position = 1
|
||
assert c2 < c1
|
||
assert not c1 < c2
|
||
assert c1 > c2
|
||
c1.position = 1
|
||
c2.position = 2
|
||
assert c1 < c2
|
||
assert c2 > c1
|
||
|
||
|
||
class ItemTest(TestCase):
|
||
"""
|
||
This test case tests various methods around the item model.
|
||
"""
|
||
|
||
def setUp(self):
|
||
self.o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
self.event = Event.objects.create(
|
||
organizer=self.o, name='Dummy', slug='dummy',
|
||
date_from=now(),
|
||
)
|
||
|
||
@classscope(attr='o')
|
||
def test_is_available(self):
|
||
i = Item.objects.create(
|
||
event=self.event, name="Ticket", default_price=23,
|
||
active=True, available_until=now() + timedelta(days=1),
|
||
)
|
||
assert i.is_available()
|
||
i.available_from = now() - timedelta(days=1)
|
||
assert i.is_available()
|
||
i.available_from = now() + timedelta(days=1)
|
||
i.available_until = None
|
||
assert not i.is_available()
|
||
i.available_from = None
|
||
i.available_until = now() - timedelta(days=1)
|
||
assert not i.is_available()
|
||
i.available_from = None
|
||
i.available_until = None
|
||
assert i.is_available()
|
||
i.active = False
|
||
assert not i.is_available()
|
||
|
||
@classscope(attr='o')
|
||
def test_availability_filter(self):
|
||
i = Item.objects.create(
|
||
event=self.event, name="Ticket", default_price=23,
|
||
active=True, available_until=now() + timedelta(days=1),
|
||
all_sales_channels=False,
|
||
)
|
||
i.limit_sales_channels.add(self.o.sales_channels.get(identifier="web"))
|
||
assert Item.objects.filter_available().exists()
|
||
assert not Item.objects.filter_available(channel='foo').exists()
|
||
|
||
i.available_from = now() - timedelta(days=1)
|
||
i.save()
|
||
assert Item.objects.filter_available().exists()
|
||
i.available_from = now() + timedelta(days=1)
|
||
i.available_until = None
|
||
i.save()
|
||
assert not Item.objects.filter_available().exists()
|
||
i.available_from = None
|
||
i.available_until = now() - timedelta(days=1)
|
||
i.save()
|
||
assert not Item.objects.filter_available().exists()
|
||
i.available_from = None
|
||
i.available_until = None
|
||
i.save()
|
||
assert Item.objects.filter_available().exists()
|
||
i.active = False
|
||
i.save()
|
||
assert not Item.objects.filter_available().exists()
|
||
|
||
cat = ItemCategory.objects.create(
|
||
event=self.event, name='Foo', is_addon=True
|
||
)
|
||
i.active = True
|
||
i.category = cat
|
||
i.save()
|
||
assert not Item.objects.filter_available().exists()
|
||
assert Item.objects.filter_available(allow_addons=True).exists()
|
||
|
||
i.category = None
|
||
i.hide_without_voucher = True
|
||
i.save()
|
||
v = Voucher.objects.create(
|
||
event=self.event, item=i,
|
||
)
|
||
assert not Item.objects.filter_available().exists()
|
||
assert Item.objects.filter_available(voucher=v).exists()
|
||
|
||
@classscope(attr='o')
|
||
def test_meta_data_inheritance(self):
|
||
prop = self.event.item_meta_properties.create(name="day", default="Monday")
|
||
i = Item.objects.create(
|
||
event=self.event, name="Ticket", default_price=23,
|
||
active=True, available_until=now() + timedelta(days=1),
|
||
)
|
||
v = i.variations.create(value="Day 1")
|
||
|
||
assert i.meta_data == {"day": "Monday"}
|
||
assert v.meta_data == {"day": "Monday"}
|
||
|
||
i.meta_values.create(property=prop, value="Tuesday")
|
||
i = Item.objects.get(pk=i.pk)
|
||
v = ItemVariation.objects.get(pk=v.pk)
|
||
|
||
assert i.meta_data == {"day": "Tuesday"}
|
||
assert v.meta_data == {"day": "Tuesday"}
|
||
|
||
v.meta_values.create(property=prop, value="Wednesday")
|
||
i = Item.objects.get(pk=i.pk)
|
||
v = ItemVariation.objects.get(pk=v.pk)
|
||
|
||
assert i.meta_data == {"day": "Tuesday"}
|
||
assert v.meta_data == {"day": "Wednesday"}
|
||
|
||
|
||
class EventTest(TestCase):
|
||
def setUp(self):
|
||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
|
||
@classscope(attr='organizer')
|
||
def test_event_end_before_start(self):
|
||
event = Event(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1)
|
||
)
|
||
with self.assertRaises(ValidationError) as context:
|
||
event.clean()
|
||
|
||
self.assertIn('date_to', str(context.exception))
|
||
|
||
@classscope(attr='organizer')
|
||
def test_presale_end_before_start(self):
|
||
event = Event(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
presale_start=now(), presale_end=now() - timedelta(hours=1)
|
||
)
|
||
with self.assertRaises(ValidationError) as context:
|
||
event.clean()
|
||
|
||
self.assertIn('presale_end', str(context.exception))
|
||
|
||
@classscope(attr='organizer')
|
||
def test_slug_validation(self):
|
||
event = Event(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc)
|
||
)
|
||
with self.assertRaises(ValidationError) as context:
|
||
event.full_clean()
|
||
|
||
self.assertIn('slug', str(context.exception))
|
||
|
||
@classscope(attr='organizer')
|
||
def test_copy(self):
|
||
prop = self.organizer.meta_properties.create(name="Language")
|
||
event1 = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='ab1234',
|
||
date_from=datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc),
|
||
date_admission=datetime.datetime(2013, 12, 26, 8, 0, 0, tzinfo=datetime.timezone.utc),
|
||
is_public=True,
|
||
)
|
||
event1.meta_values.create(property=prop, value="DE")
|
||
tr7 = event1.tax_rules.create(rate=Decimal('7.00'), default=True)
|
||
c1 = event1.categories.create(name='Tickets')
|
||
c2 = event1.categories.create(name='Workshops')
|
||
i1 = event1.items.create(name='Foo', default_price=Decimal('13.00'), tax_rule=tr7,
|
||
category=c1)
|
||
v1 = i1.variations.create(value='Bar')
|
||
i1.addons.create(addon_category=c2)
|
||
q1 = event1.quotas.create(name='Quota 1', size=50)
|
||
q1.items.add(i1)
|
||
q1.variations.add(v1)
|
||
que1 = event1.questions.create(question="Age", type="N")
|
||
que1.items.add(i1)
|
||
event1.settings.foo_setting = 23
|
||
cl1 = event1.checkin_lists.create(
|
||
name="All", all_products=False,
|
||
rules={
|
||
"and": [
|
||
{"isBefore": [{"var": "now"}, {"buildTime": ["date_from"]}, None]},
|
||
{"inList": [{"var": "product"}, {"objectList": [{"lookup": ["product", str(i1.pk), "Text"]}]}]},
|
||
{"inList": [{"var": "variation"}, {"objectList": [{"lookup": ["variation", str(v1.pk), "Text"]}]}]}
|
||
]
|
||
}
|
||
)
|
||
cl1.limit_products.add(i1)
|
||
|
||
event2 = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='ab54321',
|
||
date_from=datetime.datetime(2013, 12, 27, 9, 0, 0, tzinfo=datetime.timezone.utc),
|
||
)
|
||
event2.copy_data_from(event1)
|
||
|
||
for a in (tr7, c1, c2, i1, q1, que1, cl1):
|
||
a.refresh_from_db()
|
||
assert a.event == event1
|
||
|
||
assert event2.date_admission == datetime.datetime(2013, 12, 27, 8, 0, 0, tzinfo=datetime.timezone.utc)
|
||
assert event2.meta_data["Language"] == "DE"
|
||
|
||
trnew = event2.tax_rules.first()
|
||
assert trnew.rate == tr7.rate
|
||
c1new = event2.categories.get(name='Tickets')
|
||
c2new = event2.categories.get(name='Workshops')
|
||
i1new = event2.items.first()
|
||
assert i1new.name == i1.name
|
||
assert i1new.category == c1new
|
||
assert i1new.tax_rule == trnew
|
||
assert i1new.variations.count() == 1
|
||
assert i1new.addons.get(addon_category=c2new)
|
||
q1new = event2.quotas.first()
|
||
assert q1new.size == q1.size
|
||
assert q1new.items.get(pk=i1new.pk)
|
||
que1new = event2.questions.first()
|
||
assert que1new.type == que1.type
|
||
assert que1new.items.get(pk=i1new.pk)
|
||
assert event2.settings.foo_setting == '23'
|
||
assert event2.cached_default_tax_rule == trnew
|
||
assert event2.checkin_lists.count() == 1
|
||
clnew = event2.checkin_lists.first()
|
||
assert [i.pk for i in clnew.limit_products.all()] == [i1new.pk]
|
||
assert clnew.rules == {
|
||
"and": [
|
||
{"isBefore": [{"var": "now"}, {"buildTime": ["date_from"]}, None]},
|
||
{"inList": [{"var": "product"}, {"objectList": [{"lookup": ["product", str(i1new.pk), "Text"]}]}]},
|
||
{"inList": [{"var": "variation"},
|
||
{"objectList": [{"lookup": ["variation", str(i1new.variations.get().pk), "Text"]}]}]}
|
||
]
|
||
}
|
||
|
||
@classscope(attr='organizer')
|
||
def test_presale_has_ended(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
assert not event.presale_has_ended
|
||
assert event.presale_is_running
|
||
|
||
event.date_from = now().replace(hour=23, minute=59, second=59)
|
||
assert not event.presale_has_ended
|
||
assert event.presale_is_running
|
||
|
||
event.date_from = now() - timedelta(days=1)
|
||
assert event.presale_has_ended
|
||
assert not event.presale_is_running
|
||
|
||
event.date_to = now() + timedelta(days=1)
|
||
assert not event.presale_has_ended
|
||
assert event.presale_is_running
|
||
|
||
event.date_to = now() - timedelta(days=1)
|
||
assert event.presale_has_ended
|
||
assert not event.presale_is_running
|
||
|
||
event.presale_end = now() + timedelta(days=1)
|
||
assert not event.presale_has_ended
|
||
assert event.presale_is_running
|
||
|
||
event.presale_end = now() - timedelta(days=1)
|
||
assert event.presale_has_ended
|
||
assert not event.presale_is_running
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now(),
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True,
|
||
all_sales_channels=False)
|
||
item.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
|
||
item2 = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=False)
|
||
q.items.add(item)
|
||
q.items.add(item2)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == [q]
|
||
assert Event.annotated(Event.objects, self.organizer.sales_channels.get(identifier="bar")).first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_product_inactive(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=False)
|
||
q.items.add(item)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_product_hidden_by_voucher(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, hide_without_voucher=True)
|
||
q.items.add(item)
|
||
|
||
voucher = Voucher.objects.create(event=event, code='a', item=item, show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == [q]
|
||
|
||
voucher = Voucher.objects.create(event=event, code='b', item=item, show_hidden_items=False)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == []
|
||
|
||
voucher = Voucher.objects.create(event=event, code='c', show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == [q]
|
||
|
||
voucher = Voucher.objects.create(event=event, code='d', quota=q, show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == [q]
|
||
|
||
item2 = Item.objects.create(event=event, name='Early-bird ticket', default_price=0)
|
||
var = item2.variations.create(item=item2, value='Test', hide_without_voucher=True)
|
||
var2 = item2.variations.create(item=item2, value='Other test', hide_without_voucher=True, active=False)
|
||
q.items.remove(item)
|
||
q.items.add(item2)
|
||
q.variations.add(var)
|
||
|
||
voucher = Voucher.objects.create(event=event, code='e', item=item2, variation=var, show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == [q]
|
||
|
||
voucher = Voucher.objects.create(event=event, code='f', item=item2, variation=var2, show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == []
|
||
|
||
voucher = Voucher.objects.create(event=event, code='g', quota=q, show_hidden_items=True)
|
||
assert Event.annotated(Event.objects, "web", voucher=voucher).first().active_quotas == [q]
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_product_addon(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
|
||
cat = ItemCategory.objects.create(
|
||
event=event, name='Foo', is_addon=True
|
||
)
|
||
item.category = cat
|
||
item.save()
|
||
q.items.add(item)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_product_unavailable(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True,
|
||
available_until=now() - timedelta(days=1))
|
||
q.items.add(item)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_variation_not_in_quota(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
|
||
item.variations.create(value="foo")
|
||
q.items.add(item)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_variation(self):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Download', slug='download',
|
||
date_from=now()
|
||
)
|
||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
|
||
v = item.variations.create(value="foo", all_sales_channels=False)
|
||
item.variations.create(value="bar")
|
||
v.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
|
||
q.items.add(item)
|
||
q.variations.add(v)
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == [q]
|
||
item.available_until = now() - timedelta(days=1)
|
||
item.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
item.available_until = None
|
||
item.available_from = now() + timedelta(days=1)
|
||
item.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
item.available_until = None
|
||
item.available_from = None
|
||
item.active = False
|
||
item.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
item.active = True
|
||
item.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == [q]
|
||
assert Event.annotated(Event.objects, self.organizer.sales_channels.get(identifier="bar")).first().active_quotas == []
|
||
v.active = False
|
||
v.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
item.hide_without_voucher = True
|
||
item.save()
|
||
assert Event.annotated(Event.objects, 'web').first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_date_range_display(self):
|
||
tz = zoneinfo.ZoneInfo("Europe/Berlin")
|
||
sets = (
|
||
(
|
||
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
|
||
datetime.datetime(2025, 3, 9, 22, 0, 0, tzinfo=tz),
|
||
'Sun, March 9th, 2025',
|
||
'<time datetime="2025-03-09">Sun, March 9th, 2025</time>',
|
||
'Sun, March 9th, 2025 20:00–21:00',
|
||
'<time datetime="2025-03-09">Sun, March 9th, 2025</time> '
|
||
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:00–21:00</time>'
|
||
),
|
||
(
|
||
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
|
||
datetime.datetime(2025, 3, 10, 3, 0, 0, tzinfo=tz),
|
||
'March 9th – 10th, 2025',
|
||
'<time datetime="2025-03-09">March 9th</time> '
|
||
'<span aria-hidden="true">–</span><span class="sr-only"> until </span> '
|
||
'<time datetime="2025-03-10">10th, 2025</time>',
|
||
'March 9th – 10th, 2025 20:00–02:00',
|
||
'<time datetime="2025-03-09">March 9th</time> '
|
||
'<span aria-hidden="true">–</span><span class="sr-only"> until </span> '
|
||
'<time datetime="2025-03-10">10th, 2025</time> '
|
||
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:00–02:00</time>'
|
||
),
|
||
(
|
||
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
|
||
datetime.datetime(2025, 3, 12, 14, 0, 0, tzinfo=tz),
|
||
'March 9th – 12th, 2025',
|
||
'<time datetime="2025-03-09">March 9th</time> '
|
||
'<span aria-hidden="true">–</span><span class="sr-only"> until </span> '
|
||
'<time datetime="2025-03-12">12th, 2025</time>',
|
||
'March 9th – 12th, 2025',
|
||
'<time datetime="2025-03-09">March 9th</time> '
|
||
'<span aria-hidden="true">–</span><span class="sr-only"> until </span> '
|
||
'<time datetime="2025-03-12">12th, 2025</time>',
|
||
),
|
||
(
|
||
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
|
||
None,
|
||
'Sun, March 9th, 2025',
|
||
'<time datetime="2025-03-09">Sun, March 9th, 2025</time>',
|
||
'Sun, March 9th, 2025 20:00',
|
||
'<time datetime="2025-03-09">Sun, March 9th, 2025</time> '
|
||
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:00</time>'
|
||
),
|
||
)
|
||
|
||
for i, (df, dt, expected, expected_html, expected_with_times, expected_with_times_html) in enumerate(sets):
|
||
event = Event.objects.create(
|
||
organizer=self.organizer, name='Dummy', slug=f'dummy{i}',
|
||
date_from=df, date_to=dt,
|
||
)
|
||
assert event.get_date_range_display() == expected
|
||
assert event.get_date_range_display(as_html=True) == expected_html
|
||
assert event.get_date_range_display(try_to_show_times=True) == expected_with_times
|
||
assert event.get_date_range_display(try_to_show_times=True, as_html=True) == expected_with_times_html
|
||
|
||
|
||
class SubEventTest(TestCase):
|
||
def setUp(self):
|
||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
self.event = Event.objects.create(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||
has_subevents=True
|
||
)
|
||
self.se = SubEvent.objects.create(
|
||
name='Testsub', date_from=now(), event=self.event
|
||
)
|
||
|
||
@classscope(attr='organizer')
|
||
def test_override_prices(self):
|
||
i = Item.objects.create(
|
||
event=self.event, name="Ticket", default_price=23,
|
||
active=True, available_until=now() + timedelta(days=1),
|
||
)
|
||
SubEventItem.objects.create(item=i, subevent=self.se, price=Decimal('30.00'))
|
||
assert self.se.item_price_overrides == {
|
||
i.pk: Decimal('30.00')
|
||
}
|
||
|
||
@classscope(attr='organizer')
|
||
def test_override_var_prices(self):
|
||
i = Item.objects.create(
|
||
event=self.event, name="Ticket", default_price=23,
|
||
active=True, available_until=now() + timedelta(days=1),
|
||
)
|
||
v = i.variations.create(value='Type 1')
|
||
SubEventItemVariation.objects.create(variation=v, subevent=self.se, price=Decimal('30.00'))
|
||
assert self.se.var_price_overrides == {
|
||
v.pk: Decimal('30.00')
|
||
}
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation(self):
|
||
q = Quota.objects.create(event=self.event, name='Quota', size=2,
|
||
subevent=self.se)
|
||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
|
||
all_sales_channels=False)
|
||
item.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
|
||
q.items.add(item)
|
||
assert SubEvent.annotated(SubEvent.objects, 'web').first().active_quotas == [q]
|
||
assert SubEvent.annotated(SubEvent.objects, 'bar').first().active_quotas == []
|
||
assert SubEvent.annotated(SubEvent.objects, self.organizer.sales_channels.get(identifier="web")).first().active_quotas == [q]
|
||
assert SubEvent.annotated(SubEvent.objects, self.organizer.sales_channels.get(identifier="bar")).first().active_quotas == []
|
||
|
||
@classscope(attr='organizer')
|
||
def test_active_quotas_annotation_no_interference(self):
|
||
se2 = SubEvent.objects.create(
|
||
name='Testsub', date_from=now(), event=self.event
|
||
)
|
||
q = Quota.objects.create(event=self.event, name='Quota', size=2,
|
||
subevent=se2)
|
||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
|
||
q.items.add(item)
|
||
assert SubEvent.annotated(SubEvent.objects, 'web').filter(pk=self.se.pk).first().active_quotas == []
|
||
assert SubEvent.annotated(SubEvent.objects, 'web').filter(pk=se2.pk).first().active_quotas == [q]
|
||
|
||
@classscope(attr='organizer')
|
||
def test_best_availability(self):
|
||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test',
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
status=Order.STATUS_PAID,
|
||
datetime=now(), expires=now() + timedelta(days=10),
|
||
total=Decimal("30"), locale='en'
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o,
|
||
item=item,
|
||
subevent=self.se,
|
||
variation=None,
|
||
price=Decimal("12"),
|
||
)
|
||
self.event.settings.low_availability_percentage = 60
|
||
|
||
# 1 quota - 1 item
|
||
q = Quota.objects.create(event=self.event, name='Quota', size=1,
|
||
subevent=self.se)
|
||
q.items.add(item)
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert len(obj.active_quotas) == 1
|
||
assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, True)
|
||
|
||
# 2 quotas - 1 item. Lowest quota wins.
|
||
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2,
|
||
subevent=self.se)
|
||
q2.items.add(item)
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert len(obj.active_quotas) == 2
|
||
assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, True)
|
||
|
||
# Same, but waiting list not allowed
|
||
item.allow_waitinglist = False
|
||
item.save()
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert len(obj.active_quotas) == 2
|
||
assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, False)
|
||
|
||
# 2 quotas - 2 items. Higher quota wins since second item is only connected to second quota.
|
||
item2 = Item.objects.create(event=self.event, name='Regular ticket', default_price=10, active=True)
|
||
q2.items.add(item2)
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert len(obj.active_quotas) == 2
|
||
assert obj.best_availability == (Quota.AVAILABILITY_OK, 1, 2, True)
|
||
assert obj.best_availability_is_low
|
||
|
||
# 1 quota - 2 items. Quota is not counted twice!
|
||
q.size = 10
|
||
q.save()
|
||
q2.delete()
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert len(obj.active_quotas) == 1
|
||
assert obj.best_availability == (Quota.AVAILABILITY_OK, 9, 10, False)
|
||
assert not obj.best_availability_is_low
|
||
|
||
# Unlimited quota, but no waiting list
|
||
q.size = None
|
||
q.save()
|
||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||
assert obj.best_availability == (Quota.AVAILABILITY_OK, None, None, False)
|
||
assert not obj.best_availability_is_low
|
||
|
||
|
||
class CachedFileTestCase(TestCase):
|
||
def test_file_handling(self):
|
||
cf = CachedFile()
|
||
val = SimpleUploadedFile("testfile.txt", b"file_content")
|
||
cf.file.save("testfile.txt", val)
|
||
cf.type = "text/plain"
|
||
cf.filename = "testfile.txt"
|
||
cf.save()
|
||
assert default_storage.exists(cf.file.name)
|
||
n = cf.file.name
|
||
with default_storage.open(cf.file.name, 'r') as f:
|
||
assert f.read().strip() == "file_content"
|
||
cf.delete()
|
||
assert not default_storage.exists(n)
|
||
|
||
|
||
class CheckinListTestCase(TestCase):
|
||
def setUp(self):
|
||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=self.organizer):
|
||
self.event = Event.objects.create(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||
)
|
||
self.item1 = self.event.items.create(name="Ticket", default_price=12)
|
||
self.item2 = self.event.items.create(name="Shirt", default_price=6)
|
||
self.cl_all = self.event.checkin_lists.create(
|
||
name='All', all_products=True
|
||
)
|
||
self.cl_all_pending = self.event.checkin_lists.create(
|
||
name='Z Pending', all_products=True, include_pending=True
|
||
)
|
||
self.cl_both = self.event.checkin_lists.create(
|
||
name='Both', all_products=False
|
||
)
|
||
self.cl_both.limit_products.add(self.item1)
|
||
self.cl_both.limit_products.add(self.item2)
|
||
self.cl_tickets = self.event.checkin_lists.create(
|
||
name='Tickets', all_products=False
|
||
)
|
||
self.cl_tickets.limit_products.add(self.item1)
|
||
o = Order.objects.create(
|
||
code='FOO1', event=self.event, email='dummy@dummy.test',
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
status=Order.STATUS_PAID,
|
||
datetime=now(), expires=now() + timedelta(days=10),
|
||
total=Decimal("30"), locale='en'
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o,
|
||
item=self.item1,
|
||
variation=None,
|
||
price=Decimal("12"),
|
||
)
|
||
op2 = OrderPosition.objects.create(
|
||
order=o,
|
||
item=self.item1,
|
||
variation=None,
|
||
price=Decimal("12"),
|
||
)
|
||
op3 = OrderPosition.objects.create(
|
||
order=o,
|
||
item=self.item2,
|
||
variation=None,
|
||
price=Decimal("6"),
|
||
)
|
||
op2.checkins.create(list=self.cl_tickets)
|
||
op3.checkins.create(list=self.cl_both)
|
||
|
||
o = Order.objects.create(
|
||
code='FOO2', event=self.event, email='dummy@dummy.test',
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
status=Order.STATUS_PENDING,
|
||
datetime=now(), expires=now() + timedelta(days=10),
|
||
total=Decimal("30"), locale='en'
|
||
)
|
||
op4 = OrderPosition.objects.create(
|
||
order=o,
|
||
item=self.item2,
|
||
variation=None,
|
||
price=Decimal("6"),
|
||
)
|
||
op4.checkins.create(list=self.cl_all_pending)
|
||
|
||
@classscope(attr='organizer')
|
||
def test_attributes(self):
|
||
lists = list(self.event.checkin_lists.order_by('name'))
|
||
assert lists == [self.cl_all, self.cl_both, self.cl_tickets, self.cl_all_pending]
|
||
assert lists[0].checkin_count == 0
|
||
assert lists[0].position_count == 3
|
||
assert lists[0].percent == 0
|
||
assert lists[1].checkin_count == 1
|
||
assert lists[1].position_count == 3
|
||
assert lists[1].percent == 33
|
||
assert lists[2].checkin_count == 1
|
||
assert lists[2].position_count == 2
|
||
assert lists[2].percent == 50
|
||
assert lists[3].checkin_count == 1
|
||
assert lists[3].position_count == 4
|
||
assert lists[3].percent == 25
|
||
|
||
|
||
class SeatingTestCase(TestCase):
|
||
def setUp(self):
|
||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=self.organizer):
|
||
self.event = Event.objects.create(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||
)
|
||
self.ticket = self.event.items.create(name="Ticket", default_price=12)
|
||
self.plan = SeatingPlan.objects.create(
|
||
name="Plan", organizer=self.organizer, layout="{}"
|
||
)
|
||
self.event.seat_category_mappings.create(
|
||
layout_category='Stalls', product=self.ticket
|
||
)
|
||
self.seat_a1 = self.event.seats.create(seat_number="A1", product=self.ticket, blocked=False, x=0, y=0)
|
||
self.seat_a2 = self.event.seats.create(seat_number="A2", product=self.ticket, blocked=False, x=1, y=1)
|
||
|
||
@classscope(attr='organizer')
|
||
def test_free(self):
|
||
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
|
||
assert self.seat_a1.is_available()
|
||
assert self.seat_a2.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_blocked(self):
|
||
self.seat_a1.blocked = True
|
||
self.seat_a1.save()
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
assert self.seat_a2.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_blocked_in_proximity(self):
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_PENDING, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1
|
||
)
|
||
|
||
self.event.settings.seating_minimal_distance = 1.5
|
||
assert set(self.event.free_seats()) == set()
|
||
assert not self.seat_a1.is_available()
|
||
assert not self.seat_a2.is_available()
|
||
|
||
self.event.settings.seating_minimal_distance = 1.4
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
assert self.seat_a2.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_order_pending(self):
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_PENDING, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_order_paid(self):
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_PAID, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_order_expired(self):
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_EXPIRED, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
|
||
assert self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_cart_active(self):
|
||
CartPosition.objects.create(
|
||
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
|
||
price=23, expires=now() + timedelta(minutes=10)
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_cart_expired(self):
|
||
CartPosition.objects.create(
|
||
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
|
||
price=23, expires=now() - timedelta(minutes=10)
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
|
||
assert self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_subevent_order_pending(self):
|
||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||
self.seat_a1.subevent = se1
|
||
self.seat_a1.save()
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_PAID, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1, subevent=se1
|
||
)
|
||
assert set(se1.free_seats()) == set()
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_subevent_order_canceled(self):
|
||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||
self.seat_a1.subevent = se1
|
||
self.seat_a1.save()
|
||
o = Order.objects.create(
|
||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||
locale='en', status=Order.STATUS_CANCELED, datetime=now(),
|
||
expires=now() + timedelta(days=10),
|
||
)
|
||
OrderPosition.objects.create(
|
||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||
seat=self.seat_a1, subevent=se1
|
||
)
|
||
assert set(se1.free_seats()) == {self.seat_a1}
|
||
assert self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_subevent_cart_active(self):
|
||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||
self.seat_a1.subevent = se1
|
||
self.seat_a1.save()
|
||
CartPosition.objects.create(
|
||
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
|
||
price=23, expires=now() + timedelta(minutes=10), subevent=se1
|
||
)
|
||
assert set(se1.free_seats()) == set()
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_subevent_cart_expired(self):
|
||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||
self.seat_a1.subevent = se1
|
||
self.seat_a1.save()
|
||
CartPosition.objects.create(
|
||
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
|
||
price=23, expires=now() - timedelta(minutes=10), subevent=se1
|
||
)
|
||
assert set(se1.free_seats()) == {self.seat_a1}
|
||
assert self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_voucher_active(self):
|
||
Voucher.objects.create(
|
||
event=self.event, code='a', item=self.ticket, seat=self.seat_a1,
|
||
valid_until=now() + timedelta(minutes=10)
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a2}
|
||
assert not self.seat_a1.is_available()
|
||
|
||
@classscope(attr='organizer')
|
||
def test_voucher_expired(self):
|
||
Voucher.objects.create(
|
||
event=self.event, code='a', item=self.ticket, seat=self.seat_a1,
|
||
valid_until=now() - timedelta(minutes=10)
|
||
)
|
||
assert set(self.event.free_seats()) == {self.seat_a2, self.seat_a1}
|
||
assert self.seat_a1.is_available()
|
||
|
||
|
||
@pytest.mark.django_db
|
||
@pytest.mark.parametrize("qtype,answer,expected", [
|
||
(Question.TYPE_STRING, "a", "a"),
|
||
(Question.TYPE_TEXT, "v", "v"),
|
||
(Question.TYPE_TEXT, "waaaaay tooooo long", ValidationError),
|
||
(Question.TYPE_NUMBER, "0.9", ValidationError),
|
||
(Question.TYPE_NUMBER, "1", Decimal("1")),
|
||
(Question.TYPE_NUMBER, "3", Decimal("3")),
|
||
(Question.TYPE_NUMBER, "2.56", Decimal("2.56")),
|
||
(Question.TYPE_NUMBER, 2.45, Decimal("2.45")),
|
||
(Question.TYPE_NUMBER, 3, Decimal("3")),
|
||
(Question.TYPE_NUMBER, Decimal("4.56"), Decimal("4.56")),
|
||
(Question.TYPE_NUMBER, 100, Decimal("100")),
|
||
(Question.TYPE_NUMBER, 100.1, ValidationError),
|
||
(Question.TYPE_NUMBER, "abc", ValidationError),
|
||
(Question.TYPE_BOOLEAN, "True", True),
|
||
(Question.TYPE_BOOLEAN, "true", True),
|
||
(Question.TYPE_BOOLEAN, "False", False),
|
||
(Question.TYPE_BOOLEAN, "false", False),
|
||
(Question.TYPE_BOOLEAN, "0", False),
|
||
(Question.TYPE_BOOLEAN, "", False),
|
||
(Question.TYPE_BOOLEAN, True, True),
|
||
(Question.TYPE_BOOLEAN, False, False),
|
||
(Question.TYPE_DATE, "2018-01-16", datetime.date(2018, 1, 16)),
|
||
(Question.TYPE_DATE, datetime.date(2018, 1, 16), datetime.date(2018, 1, 16)),
|
||
(Question.TYPE_DATE, "2018-13-16", ValidationError),
|
||
(Question.TYPE_DATE, "2018-12-16", ValidationError),
|
||
(Question.TYPE_DATE, "2018-01-14", ValidationError),
|
||
(Question.TYPE_TIME, "15:20", datetime.time(15, 20)),
|
||
(Question.TYPE_TIME, datetime.time(15, 20), datetime.time(15, 20)),
|
||
(Question.TYPE_TIME, "44:20", ValidationError),
|
||
(Question.TYPE_DATETIME, "2018-01-16T15:20:00+01:00",
|
||
datetime.datetime(2018, 1, 16, 15, 20, 0, tzinfo=tzoffset(None, 3600))),
|
||
(Question.TYPE_DATETIME, "2018-01-16T14:20:00Z",
|
||
datetime.datetime(2018, 1, 16, 14, 20, 0, tzinfo=tzoffset(None, 0))),
|
||
(Question.TYPE_DATETIME, "2018-01-16T15:20:00",
|
||
datetime.datetime(2018, 1, 16, 15, 20, 0, tzinfo=tzoffset(None, 3600))),
|
||
(Question.TYPE_DATETIME, "2018-01-16T15:AB:CD", ValidationError),
|
||
(Question.TYPE_DATETIME, "2018-01-16T13:20:00+01:00", ValidationError),
|
||
(Question.TYPE_DATETIME, "2018-01-16T16:20:00+01:00", ValidationError),
|
||
])
|
||
def test_question_answer_validation(qtype, answer, expected):
|
||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=o):
|
||
event = Event.objects.create(
|
||
organizer=o, name='Dummy', slug='dummy',
|
||
date_from=now(),
|
||
)
|
||
event.settings.timezone = 'Europe/Berlin'
|
||
q = Question(
|
||
type=qtype, event=event,
|
||
valid_date_min=datetime.date(2018, 1, 15),
|
||
valid_date_max=datetime.date(2018, 12, 15),
|
||
valid_datetime_min=datetime.datetime(2018, 1, 16, 14, 0, 0, tzinfo=tzoffset(None, 3600)),
|
||
valid_datetime_max=datetime.datetime(2018, 1, 16, 16, 0, 0, tzinfo=tzoffset(None, 3600)),
|
||
valid_number_min=Decimal('1'),
|
||
valid_number_max=Decimal('100'),
|
||
valid_string_length_max=8,
|
||
)
|
||
if isinstance(expected, type) and issubclass(expected, Exception):
|
||
with pytest.raises(expected):
|
||
q.clean_answer(answer)
|
||
elif callable(expected):
|
||
assert expected(q.clean_answer(answer))
|
||
else:
|
||
assert q.clean_answer(answer) == expected
|
||
|
||
|
||
@pytest.mark.django_db
|
||
def test_question_answer_validation_localized_decimal():
|
||
q = Question(type='N')
|
||
with language("de"):
|
||
assert q.clean_answer("2,56") == Decimal("2.56")
|
||
|
||
|
||
@pytest.mark.django_db
|
||
def test_question_answer_validation_choice():
|
||
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=organizer):
|
||
event = Event.objects.create(
|
||
organizer=organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||
)
|
||
q = Question.objects.create(type='C', event=event, question='Q')
|
||
o1 = q.options.create(answer='A')
|
||
o2 = q.options.create(answer='B')
|
||
q2 = Question.objects.create(type='C', event=event, question='Q2')
|
||
o3 = q2.options.create(answer='C')
|
||
assert q.clean_answer(str(o1.pk)) == o1
|
||
assert q.clean_answer(o1.pk) == o1
|
||
assert q.clean_answer(str(o2.pk)) == o2
|
||
assert q.clean_answer(o2.pk) == o2
|
||
with pytest.raises(ValidationError):
|
||
q.clean_answer(str(o2.pk + 1000))
|
||
with pytest.raises(ValidationError):
|
||
q.clean_answer('FOO')
|
||
with pytest.raises(ValidationError):
|
||
q.clean_answer(str(o3.pk))
|
||
|
||
|
||
@pytest.mark.django_db
|
||
def test_question_answer_validation_multiple_choice():
|
||
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=organizer):
|
||
event = Event.objects.create(
|
||
organizer=organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||
)
|
||
q = Question.objects.create(type='M', event=event, question='Q')
|
||
o1 = q.options.create(answer='A')
|
||
o2 = q.options.create(answer='B')
|
||
q.options.create(answer='D')
|
||
q2 = Question.objects.create(type='M', event=event, question='Q2')
|
||
o3 = q2.options.create(answer='C')
|
||
assert q.clean_answer("{},{}".format(str(o1.pk), str(o2.pk))) == [o1, o2]
|
||
assert q.clean_answer([str(o1.pk), str(o2.pk)]) == [o1, o2]
|
||
assert q.clean_answer([str(o1.pk)]) == [o1]
|
||
assert q.clean_answer([o1.pk]) == [o1]
|
||
with pytest.raises(ValidationError):
|
||
assert q.clean_answer([o1.pk, o3.pk]) == [o1]
|
||
with pytest.raises(ValidationError):
|
||
assert q.clean_answer([o1.pk, o3.pk + 1000]) == [o1]
|
||
with pytest.raises(ValidationError):
|
||
assert q.clean_answer([o1.pk, 'FOO']) == [o1]
|
||
|
||
|
||
@pytest.mark.django_db
|
||
def test_subevent_date_updates_order_date():
|
||
# When the date of a subevent changes, all orders need to get a bumped modification date to hold
|
||
# a required invariant of the libpretixsync synchronization approach.
|
||
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
with scope(organizer=organizer):
|
||
event = Event.objects.create(
|
||
organizer=organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1), has_subevents=True
|
||
)
|
||
item1 = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
|
||
se1 = event.subevents.create(date_from=now(), name="SE 1")
|
||
se2 = event.subevents.create(date_from=now(), name="SE 2")
|
||
|
||
order1 = Order.objects.create(
|
||
event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6,
|
||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||
)
|
||
OrderPosition.objects.create(order=order1, item=item1, subevent=se1, price=2)
|
||
order2 = Order.objects.create(
|
||
event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6,
|
||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||
)
|
||
OrderPosition.objects.create(order=order2, item=item1, subevent=se2, price=2)
|
||
|
||
o1lm = order1.last_modified
|
||
o2lm = order2.last_modified
|
||
|
||
time.sleep(1)
|
||
se1.date_from += timedelta(days=2)
|
||
se1.save()
|
||
se2.name = "foo"
|
||
se2.save()
|
||
|
||
order1.refresh_from_db()
|
||
order2.refresh_from_db()
|
||
|
||
assert order1.last_modified > o1lm
|
||
assert order2.last_modified == o2lm
|
||
|
||
|
||
class ScheduledExportTestCase(TestCase):
|
||
|
||
@scopes_disabled()
|
||
def setUp(self):
|
||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||
self.event = Event.objects.create(
|
||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||
date_from=now(), date_to=now() - timedelta(hours=1), has_subevents=True
|
||
)
|
||
self.event.settings.timezone = 'Europe/Berlin'
|
||
|
||
@classscope(attr='organizer')
|
||
def test_compute_next_time(self):
|
||
s = ScheduledEventExport(event=self.event)
|
||
s.schedule_rrule = "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;COUNT=30;INTERVAL=1;WKST=MO;BYDAY=TU,TH"
|
||
s.schedule_rrule_time = datetime.time(6, 30, 0)
|
||
|
||
with freeze_time("2023-01-18 15:08:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 1, 19, 6, 30, 0, tzinfo=self.event.timezone)
|
||
|
||
with freeze_time("2023-01-19 06:28:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 1, 19, 6, 30, 0, tzinfo=self.event.timezone)
|
||
|
||
with freeze_time("2023-01-19 06:30:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 1, 24, 6, 30, 0, tzinfo=self.event.timezone)
|
||
|
||
with freeze_time("2024-01-18 15:08:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run is None
|
||
|
||
@classscope(attr='organizer')
|
||
def test_compute_next_time_handle_dst(self):
|
||
s = ScheduledEventExport(event=self.event)
|
||
s.schedule_rrule = "DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO"
|
||
s.schedule_rrule_time = datetime.time(2, 30, 0)
|
||
with freeze_time("2023-03-25 18:00:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 3, 26, 3, 30, 0, tzinfo=self.event.timezone)
|
||
with freeze_time("2023-03-26 18:00:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 3, 27, 2, 30, 0, tzinfo=self.event.timezone)
|
||
with freeze_time("2023-10-28 18:00:00+01:00"):
|
||
s.compute_next_run()
|
||
assert s.schedule_next_run == datetime.datetime(2023, 10, 29, 2, 30, 0, fold=0, tzinfo=self.event.timezone)
|