Merge branch 'master' into self-service-storno

This commit is contained in:
Lukas Bockstaller
2026-08-14 08:08:28 +02:00
358 changed files with 568764 additions and 585470 deletions
+8 -7
View File
@@ -252,13 +252,13 @@ TEST_HISTORY_RES = {
}
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
res = dict(TEST_LIST_RES)
res["id"] = clist.pk
res["limit_products"] = [item.pk]
with django_assert_num_queries(11):
with django_assert_num_queries(9):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@@ -422,7 +422,7 @@ def test_list_update(token_client, organizer, event, clist):
assert cl.name == "VIP"
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -437,7 +437,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
p3["addon_to"] = p1["id"]
# All items
with django_assert_num_queries(24):
with django_assert_num_queries(22):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
organizer.slug, event.slug, clist_all.pk
))
@@ -680,7 +680,7 @@ def _redeem(token_client, org, clist, p, body=None):
), body or {}, format='json')
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first().pk
@@ -1127,6 +1127,7 @@ def test_store_failed(token_client, organizer, clist, event, order):
organizer.slug, event.slug, clist.pk,
), {
'raw_barcode': '123456',
'raw_source_type': 'nfc_uid',
'nonce': '4321',
'error_reason': 'invalid'
}, format='json')
@@ -1367,7 +1368,7 @@ def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist,
assert ci.position == p
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -1399,7 +1400,7 @@ def test_checkin_pdf_data_requires_permission(token_client, event, team, organiz
assert not resp.data['results'][0].get('pdf_data')
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_expand(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
op = order.positions.first()
+42 -2
View File
@@ -25,6 +25,7 @@ from unittest import mock
import pytest
from django.core.files.base import ContentFile
from django.db import connection
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -36,6 +37,7 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.base.models import (
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
)
from pretix.testutils.db import readonly_db
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
@@ -214,7 +216,7 @@ def _redeem(token_client, org, clist, p, body=None, query='', headers={}):
), body, format='json', headers={})
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first()
@@ -996,7 +998,7 @@ def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, eve
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
django_assert_max_num_queries):
with scopes_disabled():
@@ -1739,3 +1741,41 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
with scopes_disabled():
rm = ReusableMedium.objects.get(identifier="0412345")
assert rm.linked_giftcard.currency == "EUR"
@pytest.mark.django_db
def test_simulate(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with scopes_disabled():
assert not p.checkins.exists()
@pytest.mark.django_db
def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
organizer.settings.reusable_media_type_nfc_uid = True
item.media_type = "nfc_uid"
item.media_policy = Item.MEDIA_POLICY_NEW
item.save()
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="nfc_uid",
identifier="12345678",
organizer=organizer,
)
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
"source_type": "barcode",
"exchange_medium_type": "nfc_uid",
"exchange_medium_identifier": "12345678",
"simulate": True,
})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'error'
with scopes_disabled():
assert not rm.linked_orderpositions.exists()
+26
View File
@@ -20,13 +20,16 @@
# <https://www.gnu.org/licenses/>.
#
import base64
from datetime import datetime, timezone
import pytest
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django_scopes import scopes_disabled
from freezegun import freeze_time
from pretix.base.models import Device
from pretix.base.models.devices import DeviceLastSeen
@pytest.fixture
@@ -386,3 +389,26 @@ def test_device_info_key_sets(device_client, device: Device):
base64.b64decode(ks['diversification_key']),
padding.PKCS1v15()
)
@pytest.mark.django_db
def test_update_last_seen(device_client, device: Device):
assert not DeviceLastSeen.objects.exists()
with freeze_time("2020-01-10T14:30:00+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
with freeze_time("2020-01-10T14:30:05+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
# No update, interal too short
device.last_seen.refresh_from_db()
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
with freeze_time("2020-01-10T14:30:30+00:00"):
resp = device_client.get('/api/v1/device/info')
assert resp.status_code == 200
device.last_seen.refresh_from_db()
assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, 30, tzinfo=timezone.utc)
+43 -5
View File
@@ -1164,6 +1164,22 @@ def test_event_update_seating(token_client, organizer, event, item, seatingplan)
assert all(seat['product'] == item.pk for seat in resp.data['results'])
@pytest.mark.django_db
def test_event_no_create_or_delete_seats(token_client, organizer, event, item, seatingplan):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/'.format(organizer.slug, event.slug),
{
'blocked': False,
},
format='json'
)
assert resp.status_code == 405
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/seats/3/'.format(organizer.slug, event.slug),
)
assert resp.status_code == 405
@pytest.mark.django_db
def test_event_update_seating_invalid_product(token_client, organizer, event, item, seatingplan):
resp = token_client.patch(
@@ -1413,11 +1429,13 @@ def test_event_create_with_seating_maps(token_client, organizer, event, meta_pro
@pytest.mark.django_db
def test_get_event_settings(token_client, organizer, event):
event.settings.imprint_url = "https://example.org"
event.settings.contact_url = "https://example.org/contact"
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
)
assert resp.status_code == 200
assert resp.data['imprint_url'] == "https://example.org"
assert resp.data['contact_url'] == "https://example.org/contact"
assert resp.data['seating_allow_blocked_seats_for_channel'] == []
resp = token_client.get(
@@ -1443,9 +1461,11 @@ def test_patch_event_settings(token_client, organizer, event, team):
team.save()
organizer.settings.imprint_url = 'https://example.org'
organizer.settings.contact_url = 'https://example.org/contact'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
{
'contact_url': 'https://example.com/contact',
'imprint_url': 'https://example.com',
'confirm_texts': [
{
@@ -1458,10 +1478,12 @@ def test_patch_event_settings(token_client, organizer, event, team):
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == "https://example.com/contact"
assert resp.data['imprint_url'] == "https://example.com"
assert resp.data['seating_allow_blocked_seats_for_channel'] == ['web']
assert not resp.data['reusable_media_active']
event.settings.flush()
assert event.settings.contact_url == 'https://example.com/contact'
assert event.settings.imprint_url == 'https://example.com'
assert event.settings.seating_allow_blocked_seats_for_channel == ['web']
assert not event.settings.reusable_media_active
@@ -1471,6 +1493,7 @@ def test_patch_event_settings(token_client, organizer, event, team):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
{
'contact_url': 'https://example.com/contact',
'imprint_url': 'https://example.com',
'confirm_texts': [
{
@@ -1513,13 +1536,16 @@ def test_patch_event_settings(token_client, organizer, event, team):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
{
'contact_url': None,
'imprint_url': None,
},
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == "https://example.org/contact"
assert resp.data['imprint_url'] == "https://example.org"
event.settings.flush()
assert event.settings.contact_url == 'https://example.org/contact'
assert event.settings.imprint_url == 'https://example.org'
resp = token_client.put(
@@ -1583,6 +1609,18 @@ def test_patch_event_settings(token_client, organizer, event, team):
@pytest.mark.django_db
def test_patch_event_settings_validation(token_client, organizer, event):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
{
'contact_url': 'invalid',
},
format='json'
)
assert resp.status_code == 400
assert resp.data == {
'contact_url': ['Enter a valid URL.']
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
{
@@ -1826,7 +1864,7 @@ def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingpl
assert not s2.blocked
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
event.settings.seating_minimal_distance = 2
@@ -1843,7 +1881,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert resp.status_code == 200
event.refresh_from_db()
with assert_num_queries(12):
with assert_num_queries(10):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1853,7 +1891,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
with scope(organizer=organizer):
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
with assert_num_queries(14):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
@@ -1861,7 +1899,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['voucher']['id'] == v0.pk
with assert_num_queries(12):
with assert_num_queries(10):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1872,7 +1910,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
with assert_num_queries(16):
with assert_num_queries(14):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
+1 -1
View File
@@ -425,7 +425,7 @@ def test_item_list(token_client, organizer, event, team, item):
@pytest.mark.django_db
def test_item_list_queries(token_client, organizer, event, team, item, item3):
with assert_num_queries(18):
with assert_num_queries(16):
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
+33 -4
View File
@@ -730,6 +730,35 @@ def test_payment_create_confirmed(token_client, organizer, event, order):
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_payment_create_confirmed_after_expiry(token_client, organizer, event, order):
djmail.outbox = []
order.expires = now() - datetime.timedelta(days=2)
order.save()
event.settings.payment_term_last = (now() - datetime.timedelta(days=2)).strftime('%Y-%m-%d')
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
organizer.slug, event.slug, order.code
), format='json', data={
'provider': 'banktransfer',
'state': 'confirmed',
'amount': order.total,
'send_email': False,
'info': {
'foo': 'bar'
},
"force": True
})
with scopes_disabled():
p = order.payments.last()
assert resp.status_code == 201
assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert p.info_data == {'foo': 'bar'}
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_payment_create_pending(token_client, organizer, event, order):
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
@@ -1058,7 +1087,7 @@ def test_orderposition_list_limited_read(
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_orderposition_list(
endpoint_template,
endpoint_type,
@@ -1166,10 +1195,10 @@ def test_orderposition_list(
'type': 'entry'
}]
if '/events/' in endpoint:
with django_assert_num_queries(18):
with django_assert_num_queries(16):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(17):
with django_assert_num_queries(15):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
@@ -2036,7 +2065,7 @@ def test_blocked_secret_list(token_client, organizer, event):
assert [res] == resp.data['results']
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
# order detail
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
+21
View File
@@ -148,6 +148,27 @@ def test_patch_settings(token_client, organizer):
)
assert resp.status_code == 400
resp = token_client.patch(
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
{
'contact_url': 'invalid'
},
format='json'
)
assert resp.status_code == 400
resp = token_client.patch(
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
{
'contact_url': 'https://example.org/contact',
},
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == 'https://example.org/contact'
organizer.settings.flush()
assert organizer.settings.contact_url == 'https://example.org/contact'
resp = token_client.patch(
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
{
+10
View File
@@ -964,6 +964,8 @@ def test_redeemed_is_not_writable(token_client, organizer, event, item):
@pytest.mark.django_db
def test_create_multiple_vouchers(token_client, organizer, event, item):
with scopes_disabled():
event.quotas.create(name="Q", size=100).items.add(item)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
@@ -1012,6 +1014,8 @@ def test_create_multiple_vouchers(token_client, organizer, event, item):
@pytest.mark.django_db
def test_create_multiple_vouchers_one_invalid(token_client, organizer, event, item):
with scopes_disabled():
event.quotas.create(name="Q", size=100).items.add(item)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
@@ -1055,6 +1059,8 @@ def test_create_multiple_vouchers_one_invalid(token_client, organizer, event, it
@pytest.mark.django_db
def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event, item):
with scopes_disabled():
event.quotas.create(name="Q", size=100).items.add(item)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
@@ -1098,6 +1104,8 @@ def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event,
@pytest.mark.django_db
def test_create_multiple_vouchers_autogenerate_codes(token_client, organizer, event, item):
with scopes_disabled():
event.quotas.create(name="Q", size=100).items.add(item)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
@@ -1157,6 +1165,8 @@ def seat1(item, event):
@pytest.mark.django_db
def test_create_multiple_vouchers_duplicate_seat(token_client, organizer, event, item, seat1, seatingplan):
with scopes_disabled():
event.quotas.create(name="Q", size=100).items.add(item)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
+3 -3
View File
@@ -732,7 +732,7 @@ def test_five_tickets_one_free(event):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("itemcount", [3, 10, 50])
def test_query_count_many_items(event, itemcount):
setup_items(event, 'Tickets', 'both', 'discounts',
@@ -784,7 +784,7 @@ def test_query_count_many_items(event, itemcount):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("catcount", [1, 10, 50])
def test_query_count_many_categories_and_discounts(event, catcount):
for n in range(1, catcount + 1):
@@ -838,7 +838,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("catcount", [2, 10, 50])
def test_query_count_many_cartpos(event, catcount):
for n in range(1, catcount + 1):
+4 -4
View File
@@ -104,7 +104,7 @@ def test_event_fail_invalid_config(event, user):
assert s.schedule_next_run > now()
assert s.error_counter == 1
assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject == "Export failed"
assert djmail.outbox[0].subject == "Scheduled export failed"
assert "Reason: Export type not found" in djmail.outbox[0].body
assert djmail.outbox[0].to == [user.email]
@@ -152,7 +152,7 @@ def test_event_fail_user_no_permission(event, user, team):
assert s.schedule_next_run > now()
assert s.error_counter == 1
assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject == "Export failed"
assert djmail.outbox[0].subject == "Scheduled export failed"
assert "Reason: Export type not found or permission denied." in djmail.outbox[0].body
assert djmail.outbox[0].to == [user.email]
@@ -235,7 +235,7 @@ def test_organizer_fail_invalid_config(event, user):
assert s.schedule_next_run > now()
assert s.error_counter == 1
assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject == "Export failed"
assert djmail.outbox[0].subject == "Scheduled export failed"
assert "Reason: Export type not found" in djmail.outbox[0].body
assert djmail.outbox[0].to == [user.email]
@@ -283,7 +283,7 @@ def test_organizer_fail_user_does_not_have_specific_permission(event, user, team
assert s.schedule_next_run > now()
assert s.error_counter == 1
assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject == "Export failed"
assert djmail.outbox[0].subject == "Scheduled export failed"
assert "Reason: Export type not found or permission denied." in djmail.outbox[0].body
assert djmail.outbox[0].to == [user.email]
+3 -1
View File
@@ -26,6 +26,7 @@ from zoneinfo import ZoneInfo
import pytest
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.timezone import now
from django_scopes import scope
from freezegun import freeze_time
@@ -210,7 +211,8 @@ def test_validate_membership_required(event, customer, membership, requiring_tic
assert "requires an active" in str(excinfo.value)
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@transaction.atomic
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries):
with django_assert_num_queries(4) as captured:
validate_memberships_in_order(
+14 -14
View File
@@ -473,7 +473,7 @@ class QuotaTestCase(BaseQuotaTestCase):
@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)
RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=True)
)
self.quota.items.add(self.item1)
self.quota.size = 1
@@ -484,7 +484,7 @@ class QuotaTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=0, time=None, base_date_name='event__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))
@@ -1192,7 +1192,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
self.order.status = Order.STATUS_EXPIRED
@@ -1347,7 +1347,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.can_modify_answers
self.op2.subevent = se3
@@ -1363,7 +1363,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last == datetime.datetime(2017, 5, 1, 23, 59, 59, tzinfo=datetime.timezone.utc)
@@ -1379,7 +1379,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last > now()
self.op2.subevent = se3
@@ -1395,7 +1395,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date == datetime.datetime(2017, 5, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
@@ -1411,7 +1411,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date > now()
self.op2.subevent = se3
@@ -1582,13 +1582,13 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__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)
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -1607,7 +1607,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -2018,13 +2018,13 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__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)
RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
@@ -2046,7 +2046,7 @@ class OrderTestCase(BaseQuotaTestCase):
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)
RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
+2 -2
View File
@@ -204,7 +204,7 @@ def test_expiry_last_relative(event):
event.date_from = now() + timedelta(days=5)
event.save()
event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
@@ -245,7 +245,7 @@ def test_expiry_last_relative_subevents(event):
)
event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2],
now_dt=today,
+6 -6
View File
@@ -133,7 +133,7 @@ def test_availability_date_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -150,7 +150,7 @@ def test_availability_start_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', RelativeDateWrapper(
RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -193,12 +193,12 @@ def test_availability_date_cart_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(cart_id="123")
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(cart_id="123")
@@ -233,11 +233,11 @@ def test_availability_date_order_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(order=order)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(order=order)
+94 -36
View File
@@ -19,13 +19,13 @@
# 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, time
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
import pytest
from django_scopes import scope
from pretix.base.models import Event, Organizer
from pretix.base.models import Event, Order, Organizer
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
TOKYO = ZoneInfo('Asia/Tokyo')
@@ -40,7 +40,6 @@ def event():
date_from=datetime(2017, 12, 27, 5, 0, 0, tzinfo=TOKYO),
presale_start=datetime(2017, 12, 1, 5, 0, 0, tzinfo=TOKYO),
plugins='pretix.plugins.banktransfer'
)
event.settings.timezone = "Asia/Tokyo"
return event
@@ -56,57 +55,57 @@ def test_absolute_date(event):
@pytest.mark.django_db
def test_relative_date_without_time(event):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None, is_after=True))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 28, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/date_from/after'
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/after'
@pytest.mark.django_db
def test_relative_date_other_base_point(event):
with scope(organizer=event.organizer):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(event) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/presale_start/'
assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
# presale_end is unset, defaults to date_from
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(event) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/presale_end/'
assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
# subevent base
se = event.subevents.create(name="SE1", date_from=datetime(2017, 11, 27, 5, 0, 0, tzinfo=TOKYO))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
# presale_start is unset on subevent, default to event
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
# presale_end is unset on all, default to date_from of subevent
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_in_minutes(event):
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60))
assert rdw.to_string() == 'RELDATE/minutes/60/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60))
assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 27, 4, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60, is_after=True))
assert rdw.to_string() == 'RELDATE/minutes/60/date_from/after'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60, is_after=True))
assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 27, 6, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_with_time(event):
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 26, 8, 5, 13, tzinfo=TOKYO)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/after'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None, is_after=True))
assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 28, 8, 5, 13, tzinfo=TOKYO)
@@ -115,35 +114,94 @@ def test_relative_date_with_time_around_dst(event):
event.settings.timezone = "Europe/Berlin"
event.date_from = datetime(2020, 3, 29, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 28, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 29, 2, 30, 0, tzinfo=BERLIN)
event.date_from = datetime(2020, 10, 25, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 24, 18, 0, 0, tzinfo=BERLIN)
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 25, 2, 30, 0, tzinfo=BERLIN)
def test_unserialize():
def test_unserialize_backwards_compatibility():
d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper.from_string(d.isoformat())
assert rdw.data == d
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
# keeping the test for the old from_string_format to ensure that we don't break anything
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
def test_backwards_compatibility():
# the data model of RelativeDate had to be extended to support other models as relation target
# previously only:
# - date_from
# - date_to
# - date_admission
# - presale_start
# - presale_end
# where valid values for the fourth slot (base_date_names) of the serialized form.
# the relationship in this case always pointed at event
# so any preexisting base_date_names without __ should continue to work and upgrade to event__{old_base_date_name}
d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper.from_string(d.isoformat())
assert rdw.data == d
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_to/')
assert rdw.to_string() == 'RELDATE/1/-/event__date_to/'
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_admission/')
assert rdw.to_string() == 'RELDATE/1/-/event__date_admission/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_start/')
assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_end/')
assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
# new order base_date_names should not work without __
with pytest.raises(TypeError):
RelativeDateWrapper.from_string('RELDATE/1/-/datetime/')
with pytest.raises(TypeError):
RelativeDateWrapper.from_string('RELDATE/1/-/expires/')
@pytest.mark.django_db
def test_relative_to_order(event):
with scope(organizer=event.organizer):
order_moment = datetime(2020, 3, 29, 18, 0, 0, tzinfo=TOKYO)
order = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=order_moment,
expires=order_moment + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='order__datetime', minutes=None, is_after=True))
assert rdw.datetime(order).astimezone(TOKYO) == datetime(2020, 3, 30, 18, 0, 0, tzinfo=TOKYO)
assert rdw.to_string() == 'RELDATE/1/-/order__datetime/after'
+1 -1
View File
@@ -235,7 +235,7 @@ class WaitingListTestCase(TestCase):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=False)
)
self.event.save()
with scope(organizer=self.o):
+7
View File
@@ -28,6 +28,7 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -82,6 +83,11 @@ def reset_locale():
translation.activate("en")
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
dirty_cache_keys.set(set())
@pytest.fixture
def fakeredis_client(monkeypatch):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
@@ -120,6 +126,7 @@ def fakeredis_client(monkeypatch):
redis = get_redis_connection("default", True)
redis.flushall()
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
monkeypatch.setattr('pretix.base.metrics.redis', redis, raising=False)
yield redis
+1 -31
View File
@@ -504,33 +504,8 @@ class Login2FAFormTest(TestCase):
assert "recovery code" in djmail.outbox[0].body
class FakeRedis(object):
def get_redis_connection(self, connection_string):
return self
def __init__(self):
self.storage = {}
def pipeline(self):
return self
def hincrbyfloat(self, rkey, key, amount):
return self
def commit(self):
return self
def exists(self, rkey):
return rkey in self.storage
def setex(self, rkey, value, expiration):
self.storage[rkey] = value
def execute(self):
pass
@pytest.mark.usefixtures("class_monkeypatch")
@pytest.mark.usefixtures("fakeredis_client")
class PasswordRecoveryFormTest(TestCase):
def setUp(self):
super().setUp()
@@ -560,11 +535,6 @@ class PasswordRecoveryFormTest(TestCase):
@override_settings(HAS_REDIS=True)
def test_email_reset_twice_redis(self):
fake_redis = FakeRedis()
m = self.monkeypatch
m.setattr('django_redis.get_redis_connection', fake_redis.get_redis_connection, raising=False)
m.setattr('pretix.base.metrics.redis', fake_redis, raising=False)
djmail.outbox = []
response = self.client.post('/control/forgot', {
+2 -2
View File
@@ -481,7 +481,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'absolute',
'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'),
'payment_term_last_2': '0',
'payment_term_last_3': 'date_from',
'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
@@ -496,7 +496,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'relative',
'payment_term_last_1': '',
'payment_term_last_2': '10',
'payment_term_last_3': 'date_from',
'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
+63 -2
View File
@@ -159,6 +159,22 @@ class OrganizerTest(SoupTest):
self.orga1.settings.flush()
assert "mail_from" not in self.orga1.settings._cache()
@staticmethod
def _fake_dmarc_record(hostname):
return {
'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;',
'bad.pretix.dev': None,
'none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_cname_record(hostname):
return {
'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.',
'pretix._domainkey.bad.pretix.dev': 'example.org',
'pretix._domainkey.none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_spf_record(hostname):
return {
@@ -172,9 +188,17 @@ class OrganizerTest(SoupTest):
'spftest.pretix.dev': None,
}[hostname]
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev")
def test_email_setup_no_verification_spf_success(self):
@override_settings(
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev",
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
)
def test_email_setup_no_verification_spf_dmarc_dkim_success(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
@@ -213,6 +237,43 @@ class OrganizerTest(SoupTest):
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dkim_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dmarc_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
def test_email_setup_smtp(self):
self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None)
self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8")
+2
View File
@@ -90,6 +90,7 @@ event_urls = [
"delete/",
"dangerzone/",
"cancel/",
"quickstart/",
"settings/",
"settings/plugins",
"settings/payment",
@@ -311,6 +312,7 @@ event_permission_urls = [
("event.settings.general:write", "live/", 200, HTTP_GET),
("event.settings.general:write", "delete/", 200, HTTP_GET),
("event.settings.general:write", "dangerzone/", 200, HTTP_GET),
("event.settings.general:write", "quickstart/", 200, HTTP_GET),
("event.settings.general:write", "settings/", 200, HTTP_GET),
# ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions
("event.settings.payment:write", "settings/payment", 200, HTTP_POST),
+28 -28
View File
@@ -230,19 +230,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -328,19 +328,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -403,19 +403,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -479,19 +479,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '02:30:00',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -567,19 +567,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -639,19 +639,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -711,19 +711,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
'rel_presale_start_6': 'date_from',
'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
'rel_presale_end_6': 'date_from',
'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
+25
View File
@@ -224,3 +224,28 @@ def test_one_view(logged_in_client, url, expected, event, item, item_category, o
)
response = logged_in_client.get(url)
assert response.status_code == expected
# Do not reintroduce any CSP nonces into control responses, as discussed in PR #6387
if response['Content-Type'] != 'application/json':
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
@pytest.mark.parametrize('url', [
'/control/login',
'/',
'/{orga}/{event}/',
])
@pytest.mark.django_db
def test_csp_header_unauthenticated(client, url, event):
# Do not reintroduce any CSP nonces into most presale responses, as discussed in PR #6387
with scope(organizer=event.organizer):
url = url.format(
event=event.slug, orga=event.organizer.slug,
)
event.live = True
event.save()
response = client.get(url)
assert response.status_code == 200
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
+437 -13
View File
@@ -35,6 +35,7 @@
import datetime
import decimal
import json
from decimal import Decimal
from django.core import mail as djmail
from django.test import TransactionTestCase
@@ -43,8 +44,8 @@ from django_scopes import scopes_disabled
from tests.base import SoupTestMixin, extract_form_fields
from pretix.base.models import (
Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota, Team,
User, Voucher, WaitingListEntry,
Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota,
SeatingPlan, Team, User, Voucher, WaitingListEntry,
)
@@ -134,49 +135,49 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
def test_filter_status_valid(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket)
doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug))
assert v.code in doc.content.decode()
v.redeemed = 1
v.save()
doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug))
assert v.code not in doc.content.decode()
def test_filter_status_redeemed(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, redeemed=1)
doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug))
assert v.code in doc.content.decode()
v.redeemed = 0
v.save()
doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug))
assert v.code not in doc.content.decode()
def test_filter_status_expired(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, valid_until=now() + datetime.timedelta(days=1))
doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug))
assert v.code not in doc.content.decode()
v.valid_until = now() - datetime.timedelta(days=1)
v.save()
doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug))
assert v.code in doc.content.decode()
def test_filter_tag(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo', tag='bar')
doc = self.client.get('/control/event/%s/%s/vouchers/?tag=bar' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=bar' % (self.orga.slug, self.event.slug))
assert 'ABCDEFG' in doc.content.decode()
doc = self.client.get('/control/event/%s/%s/vouchers/?tag=baz' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=baz' % (self.orga.slug, self.event.slug))
assert 'ABCDEFG' not in doc.content.decode()
def test_search_code(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo')
doc = self.client.get('/control/event/%s/%s/vouchers/?search=ABCDEFG' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=ABCDEFG' % (self.orga.slug, self.event.slug))
assert 'ABCDEFG' in doc.content.decode()
doc = self.client.get('/control/event/%s/%s/vouchers/?search=Foo' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=Foo' % (self.orga.slug, self.event.slug))
assert 'ABCDEFG' in doc.content.decode()
doc = self.client.get('/control/event/%s/%s/vouchers/?search=12345' % (self.orga.slug, self.event.slug))
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=12345' % (self.orga.slug, self.event.slug))
assert 'ABCDEFG' not in doc.content.decode()
def test_bulk_rng(self):
@@ -806,3 +807,426 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
assert 'walk-ins' in names
assert 'waiting-list' not in names
class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase):
@scopes_disabled()
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.orga = Organizer.objects.create(name='CCC', slug='ccc')
self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
)
t = Team.objects.create(organizer=self.orga, all_event_permissions=True)
t.members.add(self.user)
t.limit_events.add(self.event)
self.client.login(email='dummy@dummy.dummy', password='dummy')
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', default_price=12)
self.quota_shirts.items.add(self.shirt)
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value='Red')
self.shirt_blue = ItemVariation.objects.create(item=self.shirt, value='Blue')
self.quota_shirts.variations.add(self.shirt_red)
self.quota_shirts.variations.add(self.shirt_blue)
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=2)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=23)
self.quota_tickets.items.add(self.ticket)
self.url = f'/control/event/{self.orga.slug}/{self.event.slug}/vouchers/bulk_edit'
def test_simple_edit(self):
with scopes_disabled():
self.event.vouchers.create(
quota=self.quota_tickets,
max_usages=10,
price_mode="set",
value=13,
)
self.event.vouchers.create(
item=self.ticket,
max_usages=10,
price_mode="set",
value=12,
)
doc = self.post_doc(self.url, {
'__ALL': 'on',
}, follow=True)
fields = extract_form_fields(doc)
assert fields.get('bulkedit-max_usages') == '10'
assert fields.get('bulkedit-price_mode') == 'set'
assert not fields.get('bulkedit-value')
fields.update({
'_bulk': ['bulkedit__price', 'bulkeditmin_usages', 'bulkedittag', 'bulkeditshow_hidden_items'],
'bulkedit-price_mode': 'percent',
'bulkedit-value': '15',
'bulkedit-min_usages': '3',
'bulkedit-tag': 'tagged',
'bulkedit-comment': 'This is a comment', # will be ignored, as not included in _bulk
'bulkedit-show_hidden_items': '',
})
doc = self.post_doc(self.url, fields, follow=True)
assert doc.select(".alert-success")
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.price_mode == "percent"
assert v.value == Decimal("15.00")
assert v.min_usages == 3
assert v.tag == "tagged"
assert v.comment == ""
assert v.show_hidden_items is False
def _update_all(self, data: dict, expect_error: str=None):
doc = self.post_doc(self.url, {
'__ALL': 'on',
}, follow=True)
fields = extract_form_fields(doc)
fields.update(data)
doc = self.post_doc(self.url, fields, follow=True)
error_texts = [el.text for el in doc.select(".alert-danger, .has-error")]
if expect_error:
assert doc.select(".alert-danger")
assert any(expect_error in t for t in error_texts), error_texts
else:
assert doc.select(".alert-success"), error_texts
def test_change_itemvar_to_product(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets)
self.event.vouchers.create(item=self.ticket)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.ticket.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.item == self.ticket
assert not v.variation
assert not v.quota
def test_change_itemvar_to_variation(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets)
self.event.vouchers.create(item=self.ticket)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.item == self.shirt
assert v.variation == self.shirt_red
assert not v.quota
def test_change_itemvar_to_quota(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets)
self.event.vouchers.create(item=self.ticket)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'q-{self.quota_tickets.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert not v.item
assert not v.variation
assert v.quota == self.quota_tickets
def test_change_itemvar_to_all(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets)
self.event.vouchers.create(item=self.ticket)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': '',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert not v.item
assert not v.variation
assert not v.quota
def test_change_max_usages(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets, max_usages=15, redeemed=4)
self.event.vouchers.create(item=self.ticket, max_usages=15, redeemed=2)
self._update_all({
'_bulk': ['bulkeditmax_usages'],
'bulkedit-max_usages': '3',
}, expect_error="already been redeemed 4 times")
self._update_all({
'_bulk': ['bulkeditmax_usages'],
'bulkedit-max_usages': '4',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.max_usages == 4
def _requires_one_more_quota(self, data: dict, quota=None):
self._update_all(data, expect_error="no sufficient quota")
quota = quota or self.quota_tickets
quota.size += 1
quota.save()
self._update_all(data)
def test_quota_check_change_item(self):
with scopes_disabled():
self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=2, redeemed=1)
self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=3, redeemed=1)
self._requires_one_more_quota({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.ticket.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.item == self.ticket
def test_quota_check_change_variation(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1)
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1)
self._requires_one_more_quota({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}',
}, quota=self.quota_shirts)
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.item == self.shirt
assert v.variation == self.shirt_red
def test_quota_check_change_item_with_variations(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1)
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1)
self._requires_one_more_quota({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.shirt.pk}',
}, quota=self.quota_shirts)
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.item == self.shirt
assert not v.variation
def test_quota_check_change_expired_to_valid(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2)
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, valid_until=now() - datetime.timedelta(days=1))
self._requires_one_more_quota({
'_bulk': ['bulkeditvalid_until'],
'bulkedit-valid_until_0': '',
'bulkedit-valid_until_1': '',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert not v.valid_until
def test_quota_check_change_max_usages(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2)
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, redeemed=1)
self._requires_one_more_quota({
'_bulk': ['bulkeditmax_usages'],
'bulkedit-max_usages': '2',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.max_usages == 2
def test_quota_check_no_change(self):
with scopes_disabled():
# Technically overbooked, but we don't have a diff in quota
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_blue.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.variation == self.shirt_blue
def test_quota_check_change_subevent(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now())
self.quota_tickets.subevent = se1
self.quota_tickets.save()
Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3)
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
self._requires_one_more_quota({
'_bulk': ['bulkeditsubevent'],
'bulkedit-subevent': f'{se1.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.subevent == se1
def test_change_subevent_quota_invalid(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now())
self.quota_tickets.subevent = se1
self.quota_tickets.save()
v1 = self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1)
self._update_all({
'_bulk': ['bulkeditsubevent'],
'bulkedit-subevent': f'{se2.pk}',
}, expect_error="selected quota does not match the selected subevent")
self._update_all({
'_bulk': ['bulkeditsubevent'],
'bulkedit-subevent': '',
}, expect_error="has no date selected")
v1.quota = None
v1.item = self.ticket
v1.save()
self._update_all({
'_bulk': ['bulkeditsubevent'],
'bulkedit-subevent': '',
}, expect_error="If you want this voucher to block quota, you need to select a specific date")
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.subevent == se1
def test_change_missing_itemvar_with_block_quota(self):
with scopes_disabled():
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True)
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True)
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': '',
}, expect_error="You need to select a specific product or quota if this voucher should reserve")
self._update_all({
'_bulk': ['bulkedititemvar', 'bulkeditblock_quota'],
'bulkedit-itemvar': '',
'bulkedit-block_quota': '',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert not v.subevent
assert not v.block_quota
def test_change_subevent_and_quota(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now())
se2 = self.event.subevents.create(name="Bar", date_from=now())
self.quota_tickets.subevent = se1
self.quota_tickets.save()
q2 = Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3)
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1)
self._update_all({
'_bulk': ['bulkedititemvar', 'bulkeditsubevent'],
'bulkedit-subevent': f'{se2.pk}',
'bulkedit-itemvar': f'q-{q2.pk}',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.subevent == se2
assert v.quota == q2
def test_quota_check_change_block_quota(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, max_usages=3)
self._requires_one_more_quota({
'_bulk': ['bulkeditblock_quota'],
'bulkedit-block_quota': 'on',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.block_quota
def test_ignore_quota(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, max_usages=3)
self._update_all({
'_bulk': ['bulkeditblock_quota', 'bulkeditallow_ignore_quota'],
'bulkedit-block_quota': 'on',
'bulkedit-allow_ignore_quota': 'on',
})
with scopes_disabled():
for v in self.event.vouchers.all():
assert v.block_quota
assert v.allow_ignore_quota
@scopes_disabled()
def _create_seat(self, **kwargs):
plan = SeatingPlan.objects.create(
name="Plan", organizer=self.orga, layout="{}"
)
self.event.seating_plan = plan
self.event.save()
return self.event.seats.create(seat_number="A1", product=self.ticket, seat_guid="A1", **kwargs)
def test_seated_unsupported(self):
with scopes_disabled():
self.event.vouchers.create(item=self.ticket, max_usages=1, seat=self._create_seat())
self._update_all({
'_bulk': ['bulkeditmax_usages'],
'bulkedit-max_usages': '2',
}, expect_error="Changing the maximum number of usages in bulk is not supported")
self._update_all({
'_bulk': ['bulkeditsubevent'],
'bulkedit-subevent': '',
}, expect_error="Changing the date in bulk is not supported")
self._update_all({
'_bulk': ['bulkedititemvar'],
'bulkedit-itemvar': f'q-{self.quota_tickets.pk}',
}, expect_error="Changing the product to a quota is not supported")
def test_seat_changed_to_valid_needs_to_be_available(self):
with scopes_disabled():
seat = self._create_seat(blocked=True)
self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat)
self._update_all({
'_bulk': ['bulkeditvalid_until'],
'bulkedit-valid_until_0': '',
'bulkedit-valid_until_1': '',
}, expect_error="not all assigned seats of the vouchers are still available")
seat.blocked = False
seat.save()
self._update_all({
'_bulk': ['bulkeditvalid_until'],
'bulkedit-valid_until_0': '',
'bulkedit-valid_until_1': '',
})
def test_seat_changed_to_valid_needs_to_be_available_subevents(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now())
seat = self._create_seat(subevent=se1, blocked=True)
self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat, subevent=se1)
self._update_all({
'_bulk': ['bulkeditvalid_until'],
'bulkedit-valid_until_0': '',
'bulkedit-valid_until_1': '',
}, expect_error="not all assigned seats of the vouchers are still available")
seat.blocked = False
seat.save()
self._update_all({
'_bulk': ['bulkeditvalid_until'],
'bulkedit-valid_until_0': '',
'bulkedit-valid_until_1': '',
})
+32
View File
@@ -0,0 +1,32 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 decimal import Decimal
from pretix.helpers.models import _normalize_decimal
def test_normalize_decimal():
assert str(_normalize_decimal(Decimal("20.0000"))) == "20"
assert str(_normalize_decimal(Decimal("20.0001"))) == "20.0001"
assert str(_normalize_decimal(Decimal("20.0100"))) == "20.01"
assert str(_normalize_decimal(Decimal("2000000.0000"))) == "2000000"
assert str(_normalize_decimal(Decimal("0.0001"))) == "0.0001"
@@ -50,6 +50,7 @@ def env():
datetime=now(), expires=now() + timedelta(days=10),
total=23,
sales_channel=o.sales_channels.get(identifier="web"),
email='dummy@dummy.dummy'
)
o2 = Order.objects.create(
code='6789Z', event=event,
@@ -129,7 +130,7 @@ def test_assign_order_unknown(env, client):
@pytest.mark.django_db
def test_assign_order_amount_incorrect(env, client):
def test_assign_order_amount_incorrect(env, client, mailoutbox):
job = BankImportJob.objects.create(event=env[0])
trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
@@ -141,6 +142,7 @@ def test_assign_order_amount_incorrect(env, client):
assert r['status'] == 'ok'
trans.refresh_from_db()
assert trans.state == BankTransaction.STATE_VALID
assert len(mailoutbox) == 1
@pytest.mark.django_db
+151 -2
View File
@@ -22,6 +22,7 @@
import json
from datetime import timedelta
from decimal import Decimal
from unittest.mock import MagicMock
import pytest
from django.utils.timezone import now
@@ -243,6 +244,61 @@ def get_test_refund():
}
def get_test_order_review_pending():
return {'id': '806440346Y391300T',
'intent': 'CAPTURE',
'status': 'COMPLETED',
'purchase_units': [{'reference_id': 'default',
'amount': {'currency_code': 'EUR', 'value': '43.59'},
'payee': {'email_address': 'dummy-facilitator@dummy.dummy',
'merchant_id': 'G6R2B9YXADKWW'},
'description': 'Order JWJGC for PayPal v2',
'custom_id': 'Order PAYPALV2-JWJGC',
'soft_descriptor': 'MARTINFACIL',
'payments': {'captures': [{'id': '22A4162004478570J',
'status': 'PENDING',
'status_details': {
'reason': 'PENDING_REVIEW'
},
'amount': {'currency_code': 'EUR', 'value': '43.59'},
'final_capture': True,
'disbursement_mode': 'INSTANT',
'seller_protection': {'status': 'ELIGIBLE',
'dispute_categories': [
'ITEM_NOT_RECEIVED',
'UNAUTHORIZED_TRANSACTION']},
'seller_receivable_breakdown': {
'gross_amount': {'currency_code': 'EUR',
'value': '43.59'},
'paypal_fee': {'currency_code': 'EUR', 'value': '1.18'},
'net_amount': {'currency_code': 'EUR',
'value': '42.41'}},
'custom_id': 'Order PAYPALV2-JWJGC',
'links': [{
'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J',
'rel': 'self',
'method': 'GET'},
{
'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund',
'rel': 'refund',
'method': 'POST'},
{
'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
'rel': 'up',
'method': 'GET'}],
'create_time': '2022-04-28T12:00:22Z',
'update_time': '2022-04-28T12:00:22Z'}]}}],
'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
'email_address': 'dummy@dummy.dummy',
'payer_id': 'Q739JNKWH67HE',
'address': {'country_code': 'DE'}},
'create_time': '2022-04-28T11:59:59Z',
'update_time': '2022-04-28T12:00:22Z',
'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
'rel': 'self',
'method': 'GET'}]}
class Object():
pass
@@ -409,7 +465,8 @@ def test_webhook_mark_paid(env, client, monkeypatch):
order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
pp_order = Result(get_test_order())
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
mock_orders_get_request = MagicMock(return_value=pp_order)
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
@@ -497,7 +554,7 @@ def test_webhook_mark_paid(env, client, monkeypatch):
"resource_version": "2.0"
}
), content_type='application_json')
mock_orders_get_request.assert_called_once_with('806440346Y391300T')
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
@@ -688,3 +745,95 @@ def test_webhook_refund2(env, client, monkeypatch):
assert r.payment == order.payments.first()
assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
@pytest.mark.django_db
def test_webhook_pending_payment(env, client, monkeypatch):
order = env[1]
order.status = Order.STATUS_PENDING
order.save()
with scopes_disabled():
order.payments.update(state=OrderPayment.PAYMENT_STATE_CREATED)
pp_order = Result(get_test_order_review_pending())
mock_orders_get_request = MagicMock(return_value=pp_order)
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
reference="806440346Y391300T")
assert order.payments.first().state == OrderPayment.PAYMENT_STATE_CREATED
client.post('/_paypal/webhook/', json.dumps(
{
"id": "WH-0AH02875JL566842H-2AF03788XV8252724",
"create_time": "2022-04-28T12:00:37.077Z",
"resource_type": "capture",
"event_type": "PAYMENT.CAPTURE.PENDING",
"summary": "Payment pending for € 43.59 EUR",
"resource": {
"update_time": "2022-04-28T12:00:22Z",
"create_time": "2022-04-28T11:59:59Z",
"amount": {
"currency_code": "EUR",
"value": "43.59"
},
"custom_id": "Order PAYPALV2-JWJGC",
"final_capture": True,
"id": "22A4162004478570J",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5M631111V9599860P",
"method": "GET",
"rel": "self"
},
{
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5M631111V9599860P/refund",
"method": "POST",
"rel": "refund"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
"method": "GET",
"rel": "up"
}
],
"payee": {
"email_address": "sb-ybfun52428692@business.example.com",
"merchant_id": "DLM8QKDR3CFZW"
},
"seller_protection": {
"status": "NOT_ELIGIBLE"
},
"status": "PENDING",
"status_details": {
"reason": "PENDING_REVIEW"
},
"supplementary_data": {
"related_ids": {
"order_id": "9L827155WD164573M"
}
}
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-0AH02875JL566842H-2AF03788XV8252724",
"method": "GET",
"rel": "self"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-0AH02875JL566842H-2AF03788XV8252724/resend",
"method": "POST",
"rel": "resend"
}
],
"event_version": "1.0",
"resource_version": "2.0"
}
), content_type='application_json')
order = env[1]
order.refresh_from_db()
with scopes_disabled():
assert order.payments.first().state == OrderPayment.PAYMENT_STATE_PENDING
+91
View File
@@ -426,6 +426,97 @@ def test_sendmail_rule_checked_in_get_mail(event, order, item):
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_checked_in_mixed_order(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email_not_matching_status(event, order, item, item2):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
# we have no email and we are checked in
# we shouldn't trigger a fallback to order.email
p3 = order.all_positions.create(item=item, price=13)
perform_checkin(p3, clist, {})
assert clist.checkin_count == 2
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
recipients = [m.to for m in djmail.outbox]
assert [p2.attendee_email] in recipients # for p2
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email(event, order, item, item2):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
order.all_positions.create(item=item, price=13) # p3
order.all_positions.create(item=item, price=13) # p4
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
sendmail_run_rules(None)
assert len(djmail.outbox) == 2
recipients = [m.to for m in djmail.outbox]
assert [order.email] in recipients # for p3 and p4
assert [p2.attendee_email] in recipients # for p2
@pytest.mark.django_db
@scopes_disabled()
def run_restriction_test(event, order, restrictions_pass=[], restrictions_fail=[]):
+61
View File
@@ -697,6 +697,42 @@ class CartTest(CartTestMixin, TestCase):
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, 23)
def test_free_price_rounding(self):
self.ticket.free_price = True
self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '40.1234',
}, follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
cr1 = CartPosition.objects.get()
assert cr1.listed_price == Decimal('23.00')
assert cr1.custom_price_input == Decimal('40.12')
assert cr1.price == Decimal('40.12')
def test_free_price_rounding_jpy(self):
self.event.currency = "JPY"
self.event.save()
self.ticket.free_price = True
self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '40.1234',
}, follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
cr1 = CartPosition.objects.get()
assert cr1.listed_price == Decimal('23.00')
assert cr1.custom_price_input == Decimal('40.00')
assert cr1.price == Decimal('40.00')
def test_variation_inactive(self):
self.shirt_red.active = False
self.shirt_red.save()
@@ -3060,6 +3096,31 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp1.addons.count() == 3
assert all(a.price == Decimal('12.00') for a in cp1.addons.all())
@classscope(attr='orga')
def test_free_price_rounding(self):
self.event.settings.locales = ['de']
self.event.settings.locale = 'de'
self.event.currency = "JPY"
self.event.save()
self.workshop1.free_price = True
self.workshop1.save()
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), {
'cp_{}_item_{}'.format(cp1.pk, self.workshop1.pk): '1',
'cp_{}_item_{}_price'.format(cp1.pk, self.workshop1.pk): '99,99',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
assert cp1.addons.count() == 1
assert cp1.addons.first().item == self.workshop1
assert cp1.addons.first().price == Decimal('100')
@classscope(attr='orga')
def test_change_number(self):
cp1 = CartPosition.objects.create(
+11 -1
View File
@@ -81,6 +81,16 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(str(self.event.name), doc.find("h1").text)
def test_footer_contact_url_overrides_mailto(self):
self.event.settings.contact_mail = 'orga@example.org'
self.event.settings.contact_url = 'https://example.org/contact'
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
contact_link = doc.find('a', string='Contact')
self.assertIsNotNone(contact_link)
self.assertTrue(contact_link['href'].startswith('/redirect/?url=https%3A//example.org/contact'))
def test_no_session_cookie_set_on_event_index_view(self):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
@@ -1122,7 +1132,7 @@ class WaitingListTest(EventTestMixin, SoupTest):
def test_auto_disable(self):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='date_from', minutes=None, is_after=False)
RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='event__date_from', minutes=None, is_after=False)
)
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
+44 -2
View File
@@ -994,7 +994,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.refresh_from_db()
assert self.order.total == Decimal('23.00')
def test_do_not_remove_unavailable_on_adding(self):
def test_do_not_remove_timeunavailable_on_adding(self):
self.iao.max_count = 2
self.iao.save()
self.workshop1.available_until = now() - datetime.timedelta(days=1)
@@ -1036,6 +1036,48 @@ class OrderChangeAddonsTest(BaseOrdersTest):
with scopes_disabled():
assert self.ticket_pos.addons.count() == 2
def test_do_not_remove_dependencyunavailable_on_adding(self):
self.iao.max_count = 2
self.iao.save()
self.workshop1.hidden_if_item_available = self.workshop2
self.workshop1.save()
with scopes_disabled():
OrderPosition.objects.create(
order=self.order,
item=self.workshop1,
variation=None,
price=Decimal("12"),
addon_to=self.ticket_pos,
attendee_name_parts={'full_name': "Peter"}
)
self.order.total += Decimal("12")
self.order.save()
response = self.client.get(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
assert '<li>1x Workshop 1</li>' in response.content.decode()
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
},
follow=True
)
doc = BeautifulSoup(response.content.decode(), "lxml")
form_data = extract_form_fields(doc.select('.main-box form')[0])
form_data['confirm'] = 'true'
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), form_data, follow=True
)
assert 'alert-success' in response.content.decode()
with scopes_disabled():
assert self.ticket_pos.addons.count() == 2
def test_do_not_overbook_unavailable_on_adding(self):
self.iao.max_count = 1
self.iao.save()
@@ -1485,7 +1527,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.save()
self._subevent_setup()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
response = self.client.get(
+1 -1
View File
@@ -1076,7 +1076,7 @@ class OrdersTest(BaseOrdersTest):
self.event.date_from = now() + datetime.timedelta(days=3)
self.event.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(RelativeDate(
base_date_name='date_from', days=2, time=None, minutes=None
base_date_name='event__date_from', days=2, time=None, minutes=None
)))
response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
+11
View File
@@ -46,6 +46,17 @@ def test_organizer_page_shown(env, client):
assert 'MRMCD e.V.' in r.rendered_content
@pytest.mark.django_db
def test_organizer_footer_contact_url_overrides_mailto(env, client):
env[0].settings.contact_mail = 'orga@example.org'
env[0].settings.contact_url = 'https://example.org/contact'
r = client.get('/mrmcd/')
assert '/redirect/?url=https%3A//example.org/contact' in r.rendered_content
assert 'mailto:orga@example.org' not in r.rendered_content
@pytest.mark.django_db
def test_public_event_on_page(env, client):
env[1].is_public = True
+19 -15
View File
@@ -173,6 +173,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -189,8 +190,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -226,9 +227,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -239,9 +240,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_blue.pk,
'original_price': None,
"price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -278,9 +279,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
@@ -343,9 +344,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -379,6 +380,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -395,8 +397,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -439,6 +441,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -481,7 +484,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
'rate': '19.00',
'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -489,7 +492,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
'rate': '19.00',
'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -524,6 +527,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
"currency_places": 2,
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
"show_variations_expanded": False,
"display_net_prices": False,
@@ -601,7 +605,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"net": "19.52",
"tax": "3.48",
"name": "MIXED!",
"rate": "19.00",
"rate": "19",
"includes_mixed_tax_rate": True
}
+46
View File
@@ -0,0 +1,46 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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/>.
#
import os
import subprocess
import sys
import tempfile
def test_start_with_redis_down():
"""
This is a test that ensures that pretix is able to start without a running redis server,
even if one is configured.
"""
with tempfile.NamedTemporaryFile(suffix="cfg") as f:
f.write(b"[redis]\nlocation=redis://127.0.0.99:65534/2\n")
f.flush()
assert subprocess.check_call(
[
sys.executable,
os.path.join(os.path.dirname(__file__), '../manage.py'),
"noop",
],
env={
"PRETIX_CONFIG_FILE": f.name,
}
) == 0