diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index f1c525b5ea..39c16ef0ec 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -86,6 +86,7 @@ from pretix.base.settings import ( PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, ) from pretix.base.templatetags.rich_text import rich_text +from pretix.base.timemachine import time_machine_now from pretix.control.forms import ( ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField, ) @@ -606,13 +607,13 @@ class BaseQuestionsForm(forms.Form): if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice: if item.validity_dynamic_start_choice_day_limit: - max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit) + max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit) else: max_date = None - min_date = now() + min_date = time_machine_now() initial = None if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership: - if pos.used_membership.date_start >= now(): + if pos.used_membership.date_start >= time_machine_now(): initial = min_date = pos.used_membership.date_start max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days: diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 3588426096..712f8946aa 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -958,11 +958,11 @@ class Item(LoggedModel): return self.validity_fixed_from, self.validity_fixed_until elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC: tz = override_tz or self.event.timezone - requested_start = requested_start or now() + requested_start = requested_start or time_machine_now() if enforce_start_limit and not self.validity_dynamic_start_choice: - requested_start = now() + requested_start = time_machine_now() if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None: - requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit)) + requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit)) valid_until = requested_start.astimezone(tz) diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 44bfaf795e..a2ee580714 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -57,7 +57,7 @@ from pretix.base.models.items import ( from pretix.base.services.cart import CartError, CartManager, error_messages from pretix.testutils.scope import classscope from pretix.testutils.sessions import get_cart_session_key - +from .test_timemachine import TimemachineTestMixin class CartTestMixin: @scopes_disabled() @@ -4276,22 +4276,9 @@ class CartSeatingTest(CartTestMixin, TestCase): assert not CartPosition.objects.filter(cart_id=self.session_key).exists() -class CartTimemachineTest(CartTestMixin, TestCase): - @scopes_disabled() - def setUp(self): - super().setUp() - self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') - self.team1 = Team.objects.create(organizer=self.orga, can_create_events=True, can_change_event_settings=True, - can_change_items=True, all_events=True) - self.team1.members.add(self.user) - self.client.login(email='dummy@dummy.dummy', password='dummy') - - def _set_time_machine_now(self, dt): - session = self.client.session - session['timemachine_now_dt'] = str(dt) - session.save() - +class CartTimemachineTest(CartTestMixin, TimemachineTestMixin, TestCase): def test_before_presale_timemachine(self): + self._login_with_permission(self.orga) self.event.presale_start = now() + timedelta(days=1) self.event.testmode = True self.event.save() @@ -4313,6 +4300,7 @@ class CartTimemachineTest(CartTestMixin, TestCase): minutes=self.event.settings.get('reservation_time', as_type=int))) def test_after_presale_timemachine(self): + self._login_with_permission(self.orga) self.event.presale_end = now() - timedelta(days=1) self.event.testmode = True self.event.save() @@ -4334,8 +4322,8 @@ class CartTimemachineTest(CartTestMixin, TestCase): minutes=self.event.settings.get('reservation_time', as_type=int))) def test_not_yet_available_with_timemachine_in_time(self): - self.event.testmode = True - self.event.save() + self._login_with_permission(self.orga) + self._enable_test_mode() self.ticket.available_from = now() + timedelta(days=2) self.ticket.available_until = now() + timedelta(days=4) self.ticket.save() @@ -4347,8 +4335,8 @@ class CartTimemachineTest(CartTestMixin, TestCase): self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1) def test_variation_no_longer_available_with_timemachine_in_time(self): - self.event.testmode = True - self.event.save() + self._login_with_permission(self.orga) + self._enable_test_mode() self.shirt_blue.available_from = now() - timedelta(days=4) self.shirt_blue.available_until = now() - timedelta(days=2) self.shirt_blue.save() @@ -4361,8 +4349,8 @@ class CartTimemachineTest(CartTestMixin, TestCase): self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1) def test_variation_no_longer_available_with_timemachine_before(self): - self.event.testmode = True - self.event.save() + self._login_with_permission(self.orga) + self._enable_test_mode() self.shirt_blue.available_from = now() - timedelta(days=4) self.shirt_blue.available_until = now() - timedelta(days=2) self.shirt_blue.save() diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index e05ef40790..752cf4d671 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -53,8 +53,10 @@ from pretix.base.models.items import ( from pretix.base.services.cart import CartManager from pretix.base.services.orders import OrderError, _perform_order from pretix.base.services.tax import VATIDFinalError, VATIDTemporaryError +from pretix.base.timemachine import time_machine_now_assigned from pretix.testutils.scope import classscope from pretix.testutils.sessions import get_cart_session_key +from .test_timemachine import TimemachineTestMixin class BaseCheckoutTestCase: @@ -122,7 +124,7 @@ class BaseCheckoutTestCase: }] -class CheckoutTestCase(BaseCheckoutTestCase, TestCase): +class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase): def _enable_reverse_charge(self): self.tr19.eu_reverse_charge = True @@ -2545,6 +2547,98 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): assert op.valid_from.isoformat() == '2023-01-20T11:00:00+00:00' assert op.valid_until.isoformat() == '2023-01-20T13:00:00+00:00' + @freeze_time("2023-01-18 10:00:00+00:00") + def test_validity_requested_with_time_machine(self): + self._login_with_permission(self.orga) + self._enable_test_mode() + self._set_time_machine_now(now() - timedelta(days=10)) + self.ticket.available_from = now() - timedelta(days=11) + self.ticket.available_until = now() - timedelta(days=9) + self.ticket.validity_mode = Item.VALIDITY_MODE_DYNAMIC + self.ticket.validity_dynamic_duration_days = 1 + self.ticket.validity_dynamic_start_choice = True + self.ticket.validity_dynamic_start_choice_day_limit = 5 + self.ticket.save() + + with scopes_disabled(): + cr1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=42, listed_price=42, price_after_voucher=42, expires=now() + timedelta(minutes=10) + ) + + # Date too far in the future, expected to fail + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + '%s-requested_valid_from' % cr1.id: '2024-01-17', + 'email': 'admin@localhost' + }, follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + self.assertGreaterEqual(len(doc.select('.has-error')), 1) + + # Corrected request + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + '%s-requested_valid_from' % cr1.id: '2023-01-10', + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + + cr1.refresh_from_db() + assert cr1.requested_valid_from.isoformat() == '2023-01-10T00:00:00+00:00' + + self._set_payment() + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + with scopes_disabled(): + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 1) + op = OrderPosition.objects.get() + assert op.valid_from.isoformat() == '2023-01-10T00:00:00+00:00' + assert op.valid_until.isoformat() == '2023-01-10T23:59:59+00:00' + + @freeze_time("2023-01-18 10:00:00+00:00") + def test_dynamic_validity_with_time_machine(self): + self._login_with_permission(self.orga) + self._enable_test_mode() + self._set_time_machine_now(now() + timedelta(days=10)) + self.ticket.available_from = now() + timedelta(days=3) + self.ticket.validity_mode = Item.VALIDITY_MODE_DYNAMIC + self.ticket.validity_dynamic_duration_days = 1 + self.ticket.validity_dynamic_start_choice = False + self.ticket.save() + + with scopes_disabled(): + cr1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=42, listed_price=42, price_after_voucher=42, expires=now() + timedelta(minutes=10) + ) + + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + + cr1.refresh_from_db() + with time_machine_now_assigned(now() + timedelta(days=10)): + assert cr1.predicted_validity[0].isoformat() == '2023-01-28T10:00:00+00:00' + assert cr1.predicted_validity[1].isoformat() == '2023-01-28T23:59:59+00:00' + + self._set_payment() + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + with scopes_disabled(): + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 1) + op = OrderPosition.objects.get() + assert op.valid_from.isoformat() == '2023-01-28T10:00:00+00:00' + assert op.valid_until.isoformat() == '2023-01-28T23:59:59+00:00' + def test_voucher(self): with scopes_disabled(): v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, price_mode='set', @@ -3486,6 +3580,28 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): doc = BeautifulSoup(response.content.decode(), "lxml") self.assertEqual(len(doc.select(".thank-you")), 1) + def test_before_presale_timemachine(self): + self._login_with_permission(self.orga) + self._enable_test_mode() + self._set_time_machine_now(now() + timedelta(days=4)) + self.event.presale_start = now() + timedelta(days=3) + self.event.save() + with scopes_disabled(): + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + + self._set_payment() + response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + assert "test mode" in response.content.decode() + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + self.assertEqual(len(doc.select(".thank-you")), 1) + with scopes_disabled(): + assert Order.objects.last().testmode + assert Order.objects.last().code[1] == "0" + def test_create_testmode_order_in_testmode(self): self.event.testmode = True self.event.save() diff --git a/src/tests/presale/test_timemachine.py b/src/tests/presale/test_timemachine.py new file mode 100644 index 0000000000..066232fcf8 --- /dev/null +++ b/src/tests/presale/test_timemachine.py @@ -0,0 +1,43 @@ +# +# 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 . +# +# 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 +# . +# +from django_scopes.state import scopes_disabled + +from pretix.base.models import User, Team + + +class TimemachineTestMixin: + @scopes_disabled() + def _login_with_permission(self, orga): + self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') + self.team1 = Team.objects.create(organizer=orga, can_create_events=True, can_change_event_settings=True, + can_change_items=True, all_events=True) + self.team1.members.add(self.user) + self.client.login(email='dummy@dummy.dummy', password='dummy') + + def _set_time_machine_now(self, dt): + session = self.client.session + session['timemachine_now_dt'] = str(dt) + session.save() + + def _enable_test_mode(self): + self.event.testmode = True + self.event.save()