forked from CGM_Public/pretix_original
Allow to define ticket validity through a product (#3105)
This commit is contained in:
@@ -296,6 +296,15 @@ TEST_ITEM_RES = {
|
||||
"grant_membership_duration_like_event": True,
|
||||
"grant_membership_duration_days": 0,
|
||||
"grant_membership_duration_months": 0,
|
||||
"validity_mode": None,
|
||||
"validity_fixed_from": None,
|
||||
"validity_fixed_until": None,
|
||||
"validity_dynamic_duration_minutes": None,
|
||||
"validity_dynamic_duration_hours": None,
|
||||
"validity_dynamic_duration_days": None,
|
||||
"validity_dynamic_duration_months": None,
|
||||
"validity_dynamic_start_choice": False,
|
||||
"validity_dynamic_start_choice_day_limit": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2253,6 +2253,101 @@ def test_order_paid_require_payment_method(token_client, organizer, event, item,
|
||||
assert not o.payments.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_validity(token_client, organizer, event, item, quota, question):
|
||||
item.validity_mode = 'dynamic'
|
||||
item.validity_dynamic_duration_minutes = 30
|
||||
item.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
del res['positions'][0]['price']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert now() - datetime.timedelta(seconds=30) < p.valid_from <= now()
|
||||
assert now() + datetime.timedelta(minutes=29) < p.valid_until < now() + datetime.timedelta(minutes=31)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_manual_validity_precedence(token_client, organizer, event, item, quota, question):
|
||||
item.validity_mode = 'dynamic'
|
||||
item.validity_dynamic_duration_minutes = 30
|
||||
item.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['valid_from'] = '2022-01-01T09:00:00.000Z'
|
||||
res['positions'][0]['valid_until'] = '2022-01-03T09:00:00.000Z'
|
||||
del res['positions'][0]['price']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert p.valid_from.isoformat() == '2022-01-01T09:00:00+00:00'
|
||||
assert p.valid_until.isoformat() == '2022-01-03T09:00:00+00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_validity_with_requested_start(token_client, organizer, event, item, quota, question):
|
||||
item.validity_mode = 'dynamic'
|
||||
item.validity_dynamic_duration_minutes = 30
|
||||
item.validity_dynamic_start_choice = True
|
||||
item.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['requested_valid_from'] = '2039-01-01T09:00:00.000Z'
|
||||
del res['positions'][0]['price']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert p.valid_from.isoformat() == '2039-01-01T09:00:00+00:00'
|
||||
assert p.valid_until.isoformat() == '2039-01-01T09:30:00+00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_validity_with_requested_start_limitation(token_client, organizer, event, item, quota, question):
|
||||
item.validity_mode = 'dynamic'
|
||||
item.validity_dynamic_duration_minutes = 30
|
||||
item.validity_dynamic_start_choice = True
|
||||
item.validity_dynamic_start_choice_day_limit = 24
|
||||
item.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['requested_valid_from'] = (now() + datetime.timedelta(days=30)).isoformat()
|
||||
del res['positions'][0]['price']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert now() + datetime.timedelta(days=23) < p.valid_from <= now() + datetime.timedelta(days=26)
|
||||
assert p.valid_until == p.valid_from + datetime.timedelta(minutes=30)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_auto_pricing(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
118
src/tests/base/test_item_validity.py
Normal file
118
src/tests/base/test_item_validity.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
from pretix.base.models import Item
|
||||
|
||||
tz = pytz.timezone("Europe/Berlin")
|
||||
|
||||
|
||||
def dt(*args, is_dst=None, **kwargs):
|
||||
return tz.localize(datetime(*args, **kwargs), is_dst=is_dst)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("minutes,hours,days,months,start,expected_end", [
|
||||
# Simple cases
|
||||
(0, 0, 0, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 10, 30, 0)), # zero case
|
||||
(10, 0, 0, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 10, 40, 0)), # "10 minute pass"
|
||||
(0, 1, 0, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 11, 30, 0)), # "hour pass"
|
||||
(10, 1, 0, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 11, 40, 0)), # "1h 10min pass"
|
||||
(0, 0, 1, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 23, 59, 59)), # "day pass"
|
||||
(0, 0, 3, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 11, 23, 59, 59)), # "3-day pass"
|
||||
(30, 6, 3, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 12, 6, 29, 59)), # "3-day pass with day end at 6:30"
|
||||
(0, 0, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 9, 23, 59, 59)), # "month pass"
|
||||
(0, 0, 3, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 12, 23, 59, 59)), # "month pass + 3 days"
|
||||
(30, 6, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 10, 6, 29, 59)), # "month pass with day end at 6:30"
|
||||
(30, 6, 1, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 11, 6, 29, 59)), # "month pass + 1 day with day end at 6:30"
|
||||
(0, 0, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 9, 23, 59, 59)), # "year pass"
|
||||
(30, 6, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 10, 6, 29, 59)), # "year pass with day end at 6:30"
|
||||
|
||||
# Calendrical edge cases
|
||||
|
||||
# Multi-day across a DST change
|
||||
(0, 0, 2, 0, dt(2023, 3, 25, 10, 30, 0), dt(2023, 3, 26, 23, 59, 59)),
|
||||
|
||||
# Month + day across a DST change
|
||||
(0, 0, 1, 1, dt(2023, 2, 25, 10, 30, 0), dt(2023, 3, 26, 23, 59, 59)),
|
||||
|
||||
# Day + hour with possibly non-existant end time during DST change
|
||||
(30, 2, 1, 0, dt(2023, 3, 25, 10, 30, 0), dt(2023, 3, 26, 3, 29, 59)),
|
||||
|
||||
# Day + hour with ambiguous end time during DST change
|
||||
(30, 2, 1, 0, dt(2023, 10, 28, 10, 30, 0), dt(2023, 10, 29, 2, 29, 59, is_dst=True)),
|
||||
|
||||
# Month with short month following
|
||||
(0, 0, 0, 1, dt(2023, 1, 31, 10, 30, 0), dt(2023, 2, 28, 23, 59, 59)),
|
||||
|
||||
# Interaction on months and leap days
|
||||
(0, 0, 0, 1, dt(2024, 1, 31, 10, 30, 0), dt(2024, 2, 29, 23, 59, 59)),
|
||||
(0, 0, 0, 12, dt(2024, 2, 29, 10, 30, 0), dt(2025, 2, 28, 23, 59, 59)),
|
||||
(0, 0, 0, 12, dt(2024, 1, 31, 10, 30, 0), dt(2025, 1, 31, 23, 59, 59)),
|
||||
])
|
||||
def test_dynamic_validity(minutes, hours, days, months, start, expected_end):
|
||||
i = Item(
|
||||
validity_mode="dynamic",
|
||||
validity_dynamic_start_choice=True,
|
||||
validity_dynamic_duration_minutes=minutes,
|
||||
validity_dynamic_duration_hours=hours,
|
||||
validity_dynamic_duration_days=days,
|
||||
validity_dynamic_duration_months=months,
|
||||
)
|
||||
assert i.compute_validity(requested_start=start, override_tz=tz) == (start, expected_end)
|
||||
|
||||
|
||||
def test_fixed_validity():
|
||||
i = Item(
|
||||
validity_mode="fixed",
|
||||
validity_fixed_from=dt(2023, 2, 9, 10, 15, 0),
|
||||
validity_fixed_until=dt(2023, 2, 9, 12, 15, 0),
|
||||
)
|
||||
assert i.compute_validity(requested_start=dt(2024, 1, 1, 0, 0, 0), override_tz=tz) == (
|
||||
i.validity_fixed_from, i.validity_fixed_until
|
||||
)
|
||||
|
||||
|
||||
def test_fixed_validity_one_sided():
|
||||
i = Item(
|
||||
validity_mode="fixed",
|
||||
validity_fixed_from=dt(2023, 2, 9, 10, 15, 0),
|
||||
validity_fixed_until=None,
|
||||
)
|
||||
assert i.compute_validity(requested_start=dt(2024, 1, 1, 0, 0, 0), override_tz=tz) == (i.validity_fixed_from, None)
|
||||
i = Item(
|
||||
validity_mode="fixed",
|
||||
validity_fixed_from=None,
|
||||
validity_fixed_until=dt(2023, 2, 9, 10, 15, 0),
|
||||
)
|
||||
assert i.compute_validity(requested_start=dt(2024, 1, 1, 0, 0, 0), override_tz=tz) == (None, i.validity_fixed_until)
|
||||
|
||||
|
||||
def test_default_validity():
|
||||
i = Item(
|
||||
validity_mode=None,
|
||||
validity_fixed_from=dt(2023, 2, 9, 10, 15, 0),
|
||||
validity_fixed_until=dt(2023, 2, 9, 12, 15, 0),
|
||||
)
|
||||
assert i.compute_validity(requested_start=dt(2024, 1, 1, 0, 0, 0), override_tz=tz) == (None, None)
|
||||
@@ -32,11 +32,12 @@
|
||||
# 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.
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
|
||||
from django.utils import translation
|
||||
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.base.i18n import language
|
||||
from pretix.helpers.daterange import daterange, datetimerange
|
||||
|
||||
|
||||
def test_same_day_german():
|
||||
@@ -147,3 +148,33 @@ def test_different_dates_other_lang():
|
||||
assert daterange(df, dt) == "01 Şubat 2003 – 03 Nisan 2005"
|
||||
assert daterange(df, dt, as_html=True) == '<time datetime="2003-02-01">01 Şubat 2003</time> – ' \
|
||||
'<time datetime="2005-04-03">03 Nisan 2005</time>'
|
||||
|
||||
|
||||
def test_datetime_same_day():
|
||||
with translation.override('de'):
|
||||
df = datetime(2003, 2, 1, 9, 0)
|
||||
dt = datetime(2003, 2, 1, 10, 0)
|
||||
assert datetimerange(df, dt) == "01.02.2003 09:00 – 10:00"
|
||||
assert datetimerange(df, dt, as_html=True) == '<time datetime="2003-02-01 09:00">01.02.2003 09:00</time> – ' \
|
||||
'<time datetime="2003-02-01 10:00">10:00</time>'
|
||||
with language('en', 'US'):
|
||||
df = datetime(2003, 2, 1, 9, 0)
|
||||
dt = datetime(2003, 2, 1, 10, 0)
|
||||
assert datetimerange(df, dt) == "02/01/2003 9 a.m. – 10 a.m."
|
||||
assert datetimerange(df, dt, as_html=True) == '<time datetime="2003-02-01 09:00">02/01/2003 9 a.m.</time> – ' \
|
||||
'<time datetime="2003-02-01 10:00">10 a.m.</time>'
|
||||
|
||||
|
||||
def test_datetime_different_day():
|
||||
with translation.override('de'):
|
||||
df = datetime(2003, 2, 1, 9, 0)
|
||||
dt = datetime(2003, 2, 2, 10, 0)
|
||||
assert datetimerange(df, dt) == "01.02.2003 09:00 – 02.02.2003 10:00"
|
||||
assert datetimerange(df, dt, as_html=True) == '<time datetime="2003-02-01 09:00">01.02.2003 09:00</time> – ' \
|
||||
'<time datetime="2003-02-02 10:00">02.02.2003 10:00</time>'
|
||||
with language('en', 'US'):
|
||||
df = datetime(2003, 2, 1, 9, 0)
|
||||
dt = datetime(2003, 2, 2, 10, 0)
|
||||
assert datetimerange(df, dt) == "02/01/2003 9 a.m. – 02/02/2003 10 a.m."
|
||||
assert datetimerange(df, dt, as_html=True) == '<time datetime="2003-02-01 09:00">02/01/2003 9 a.m.</time> – ' \
|
||||
'<time datetime="2003-02-02 10:00">02/02/2003 10 a.m.</time>'
|
||||
|
||||
@@ -38,6 +38,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from freezegun import freeze_time
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
@@ -2353,6 +2354,98 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.price, 24)
|
||||
|
||||
@freeze_time("2023-01-18 03:00:00+01:00")
|
||||
def test_validity_requested_start_date(self):
|
||||
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 = 30
|
||||
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-20',
|
||||
'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-20',
|
||||
'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-20T00: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-20T00:00:00+00:00'
|
||||
assert op.valid_until.isoformat() == '2023-01-20T23:59:59+00:00'
|
||||
|
||||
@freeze_time("2023-01-18 03:00:00+01:00")
|
||||
def test_validity_requested_start_date_and_time(self):
|
||||
self.ticket.validity_mode = Item.VALIDITY_MODE_DYNAMIC
|
||||
self.ticket.validity_dynamic_duration_hours = 2
|
||||
self.ticket.validity_dynamic_start_choice = True
|
||||
self.ticket.validity_dynamic_start_choice_day_limit = 30
|
||||
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_0' % cr1.id: '2024-01-20',
|
||||
'%s-requested_valid_from_1' % cr1.id: '11:00:00',
|
||||
'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_0' % cr1.id: '2023-01-20',
|
||||
'%s-requested_valid_from_1' % cr1.id: '11:00:00',
|
||||
'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-20T11: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-20T11:00:00+00:00'
|
||||
assert op.valid_until.isoformat() == '2023-01-20T13:00:00+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',
|
||||
|
||||
Reference in New Issue
Block a user