Compare commits

..

8 Commits

Author SHA1 Message Date
luelista
c808afc2e9 Apply suggestions from code review
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-05-12 17:46:36 +02:00
Mira Weller
baf7e098b6 Old Python compat 2026-04-28 15:23:57 +02:00
Mira Weller
a78e2f3ceb add meta property handling to cross-organizer event clone test case 2026-04-28 14:40:58 +02:00
Mira Weller
2fdd9f991c handle new duplicates in EventMetaValue.{event,property} 2026-04-28 14:40:58 +02:00
Mira Weller
111127c0fa add migrations with fixups 2026-04-28 14:09:31 +02:00
Mira Weller
f1a4b51604 add unique_together on EventMetaProperty (organizer, name) 2026-04-28 14:03:50 +02:00
Mira Weller
c9d29bbca5 Adapt event meta_properties when cloning across organizers 2026-04-28 13:33:06 +02:00
Mira Weller
bfdf959fad Validate that event and property belong to the same organizer 2026-04-28 13:33:06 +02:00
20 changed files with 139 additions and 260 deletions

View File

@@ -117,7 +117,7 @@ dev = [
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=1.3.0",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",

View File

@@ -1658,7 +1658,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
count_waitinglist=False,
force=request.data.get('force', False),
send_mail=send_mail,
ignore_date=True,
)
except Quota.QuotaExceededException:
pass
@@ -1694,8 +1693,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=self.request.auth,
count_waitinglist=False,
send_mail=send_mail,
force=force,
ignore_date=True)
force=force)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:

View File

@@ -0,0 +1,81 @@
# Generated by Django 5.2.12 on 2026-04-28 11:34
import logging
from django.db import IntegrityError, migrations, transaction
from django.db.models import Count, F
logger = logging.getLogger(__name__)
def fix_cross_organizer_eventmetavalues(apps, schema_editor):
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
cross_org_values = EventMetaValue.objects.filter(event__organizer__pk__ne=F('property__organizer__pk'))
for emv in cross_org_values:
logger.info(f"Fixup cross-organizer value {emv.event.organizer.slug}/{emv.event.slug}\n before: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {emv.value}")
try:
emv.property = emv.event.organizer.meta_properties.filter(name=emv.property.name).first()
logger.info(f" found existing EventMetaProperty in {emv.event.organizer.slug}")
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
logger.info(f" EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
emv.delete()
continue
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = emv.event.organizer
meta_prop.save(force_insert=True)
logger.info(f" created new EventMetaProperty")
emv.property = meta_prop
logger.info(f" after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {emv.value}")
emv.save(update_fields=["property"])
def make_eventmetaproperties_unique(apps, schema_editor):
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1)
for dup in duplicates:
logger.info(f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
# TODO: any better idea than picking the property to keep more or less randomly (first in database order)?
target = props[0]
invalid = props[1:]
try:
with transaction.atomic():
affected = EventMetaValue.objects.filter(
event__organizer=dup['organizer'], property__in=invalid
).update(
property=target
)
logger.info(f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
except IntegrityError as e:
logger.info(f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
logger.info(f" {e}")
for prop in invalid:
newname = f'{prop.name}_DUPLICATE_{prop.id}'
logger.info(f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
prop.name = newname
prop.save()
else:
for prop in invalid:
logger.info(f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
prop.delete()
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_pluggable_permissions"),
]
operations = [
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.12 on 2026-04-28 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0299_fixup_eventmetaproperties"),
]
operations = [
migrations.AlterUniqueTogether(
name="eventmetaproperty",
unique_together={("organizer", "name")},
),
]

View File

@@ -715,12 +715,6 @@ class Event(EventMixin, LoggedModel):
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
self.settings.mail_send_order_free_attendee = True
self.settings.mail_send_order_placed_attendee = True
self.settings.mail_send_order_paid_attendee = True
self.settings.mail_send_order_approved_attendee = True
self.settings.mail_send_order_approved_free_attendee = True
self.settings.mail_text_download_reminder_attendee = True
@property
def social_image(self):
@@ -883,6 +877,8 @@ class Event(EventMixin, LoggedModel):
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
)
is_cross_organizer = other.organizer_id != self.organizer_id
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
# Plugins can create data in installed() hook based on existing data of the event.
# Calling set_active_plugins() results in defaults being created while actually data
@@ -911,6 +907,15 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
if is_cross_organizer:
try:
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = self.organizer
meta_prop.save(force_insert=True)
emv.property = meta_prop
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
@@ -964,13 +969,13 @@ class Event(EventMixin, LoggedModel):
if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id]
if i.grant_membership_type and other.organizer_id != self.organizer_id:
if i.grant_membership_type and is_cross_organizer:
i.grant_membership_type = None
i.save() # no force_insert since i.picture.save could have already inserted
i.log_action('pretix.object.cloned')
if require_membership_types and other.organizer_id == self.organizer_id:
if require_membership_types and not is_cross_organizer:
i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels:
@@ -985,7 +990,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {}
v.save(force_insert=True)
if require_membership_types and other.organizer_id == self.organizer_id:
if require_membership_types and not is_cross_organizer:
v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
@@ -1836,6 +1841,7 @@ class EventMetaProperty(LoggedModel):
class Meta:
ordering = ("position", "name",)
unique_together = ('organizer', 'name')
@property
def choice_keys(self):
@@ -1869,6 +1875,8 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear()
def save(self, *args, **kwargs):
if self.event and self.event.organizer != self.property.organizer:
raise ValidationError(_("Property and event must belong to the same organizer."))
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()

View File

@@ -38,7 +38,6 @@ SOURCE_NAMES = {
None: _('European Central Bank'), # backwards-compatibility
'eu:ecb:eurofxref-daily': _('European Central Bank'),
'cz:cnb:rate-fixing-daily': _('Czech National Bank'),
'pl:nbp:table-a': _('National Bank of Poland'),
}
@@ -50,7 +49,6 @@ def fetch_rates(sender, **kwargs):
source_tasks = {
'eu:ecb:eurofxref-daily': fetch_ecb_rates,
'cz:cnb:rate-fixing-daily': fetch_cnb_cz_rates,
'pl:nbp:table-a': fetch_nbp_pl_rates,
}
for source_name, task in source_tasks.items():
@@ -146,29 +144,3 @@ def fetch_cnb_cz_rates():
rate=rate,
)
)
@app.task()
def fetch_nbp_pl_rates():
"""
Fetches currency rates from the Polish National Bank.
"""
r = requests.get("https://api.nbp.pl/api/exchangerates/tables/A/", headers={
"Accept": "application/json",
})
r.raise_for_status()
data = r.json()[0]
source_date = datetime.strptime(data["effectiveDate"], "%Y-%m-%d").date()
for r in data["rates"]:
rate = Decimal(r["mid"]).quantize(Decimal('0.000001'))
ExchangeRate.objects.update_or_create(
source='pl:nbp:table-a',
source_currency=r["code"],
other_currency='PLN',
defaults=dict(
source_date=source_date,
rate=rate,
)
)

View File

@@ -205,19 +205,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'cz:cnb:rate-fixing-daily'
elif invoice.event.settings.invoice_eu_currencies == 'PLN' and invoice.event.currency != 'PLN':
invoice.foreign_currency_display = 'PLN'
if settings.FETCH_ECB_RATES:
rate = ExchangeRate.objects.filter(
source='pl:nbp:table-a',
source_currency=invoice.event.currency,
other_currency=invoice.foreign_currency_display,
source_date__gt=now().date() - timedelta(days=7)
).first()
if rate:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'pl:nbp:table-a'
except InvoiceAddress.DoesNotExist:
ia = None

View File

@@ -574,7 +574,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
'serializer_kwargs': dict(
@@ -583,7 +582,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
},

View File

@@ -28,7 +28,7 @@ from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
@@ -102,16 +102,6 @@ class SubEventBulkForm(SubEventForm):
required=False,
limit_choices=('date_from', 'date_to'),
)
skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
help_text=pgettext_lazy(
'subevent',
'This can be useful if all your dates happen in the same location and no repeated dates should '
'be created in conflict with existing special events. This respects even inactive dates and works best if '
'all dates have both a start and end time.'
),
required=False,
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']

View File

@@ -379,8 +379,6 @@
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
</p>
</div>
<hr />
{% bootstrap_field form.skip_if_overlap layout="control" horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
</fieldset>
<fieldset>
<legend>{% trans "General information" %}</legend>

View File

@@ -917,35 +917,6 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
if len(subevents) > 100_000:
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
if form.cleaned_data.get("skip_if_overlap") and subevents:
def overlaps(a_from, a_to, b_from, b_to):
if a_from == b_from:
return True
if a_from > b_from:
# a starts after b
# check if it starts before b ends
return b_to and a_from < b_to
# a starts before b
# check if it ends before b starts
return a_to and a_to > b_from
date_min = min(se.date_from for se in subevents)
date_max = max(se.date_to or se.date_from for se in subevents)
dates_existing = list(self.request.event.subevents.annotate(
date_fromto=Coalesce('date_to', 'date_from'),
).filter(
date_from__lte=date_max,
date_fromto__gte=date_min,
).values('date_from', 'date_to'))
subevents = [
se for se in subevents if not any(
overlaps(se.date_from, se.date_to, other['date_from'], other['date_to'])
for other in dates_existing
)
]
if not subevents:
raise ValidationError(_('All dates would be skipped because they conflict with existing dates.'))
for i, se in enumerate(subevents):
se.save(clear_cache=False)
if i % 100 == 0:

View File

@@ -316,7 +316,7 @@ def nav_context_list(request):
page = 1
qs_events = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) | Q(domain__domainname__iexact=query)
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
@@ -331,7 +331,7 @@ def nav_context_list(request):
else:
qs_orga = Organizer.objects.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if query:
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query) | Q(domains__domainname__iexact=query))
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
qs_orga = qs_orga.annotate(
n_events=Count("events")
).order_by("-n_events")
@@ -619,7 +619,7 @@ def checkinlist_select2(request, **kwargs):
qs = request.event.checkin_lists.select_related('subevent').filter(
qf
).order_by('subevent__date_from', 'name', 'pk')
).order_by('name')
total = qs.count()
pagesize = 20

View File

@@ -114,10 +114,8 @@ class CartMixin:
return cached_invoice_address(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False) and not order:
# The user has no cart, so we can save a lot of work
if not self.request.session.session_key and not order:
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
@@ -379,13 +377,9 @@ def cart_exists(request):
from pretix.presale.views.cart import get_or_create_cart_id
if not hasattr(request, '_cart_cache'):
cid = get_or_create_cart_id(request, create=False)
if cid:
return CartPosition.objects.filter(
cart_id=cid, event=request.event
).exists()
else:
return False
return CartPosition.objects.filter(
cart_id=get_or_create_cart_id(request), event=request.event
).exists()
return bool(request._cart_cache)

View File

@@ -71,7 +71,6 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
});
var form_handlers = function (el) {
el.trigger("rescan.areYouSure");
el.find("[data-formset]").formset(
{
animateForms: true,

View File

@@ -966,7 +966,6 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
width: 80vw;
max-width: 1080px;
height: 80vh;
max-height: 100dvh;
}
.pretix-widget-frame-inner iframe {
width: 100% !important;

View File

@@ -730,34 +730,6 @@ 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'
}
})
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(

View File

@@ -238,6 +238,9 @@ def test_full_clone_cross_organizer_differences():
sc1_c = organizer.sales_channels.create(identifier="c")
sc2_a = organizer2.sales_channels.get(identifier="web")
sc2_c = organizer2.sales_channels.create(identifier="c")
o1_meta_prop_a = organizer.meta_properties.create(name="Prop to copy")
o1_meta_prop_b = organizer.meta_properties.create(name="Prop to find")
o2_meta_prop_b = organizer2.meta_properties.create(name="Prop to find")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
@@ -262,6 +265,9 @@ def test_full_clone_cross_organizer_differences():
event.settings.payment_giftcard__enabled = True
event.settings.payment_giftcard__restrict_to_sales_channels = ['web', 'b', 'c']
event.meta_values.create(property=o1_meta_prop_a, value='a')
event.meta_values.create(property=o1_meta_prop_b, value='b')
copied_event = Event.objects.create(
organizer=organizer2, name='Dummy2', slug='dummy2',
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
@@ -284,3 +290,9 @@ def test_full_clone_cross_organizer_differences():
assert event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'b', 'c']
assert copied_event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'c']
assert event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).value == 'a'
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer2
assert copied_event.meta_values.get(property=o2_meta_prop_b).value == 'b'
assert copied_event.meta_values.get(property=o2_meta_prop_b).property.organizer == organizer2

View File

@@ -123,8 +123,6 @@ def env():
ExchangeRate.objects.create(source_date=date.today(), source='eu:ecb:eurofxref-daily', source_currency='EUR', other_currency=currency, rate=rate)
ExchangeRate.objects.create(source_date=date.today(), source='cz:cnb:rate-fixing-daily', source_currency='EUR',
other_currency='CZK', rate=Decimal('25.0000'))
ExchangeRate.objects.create(source_date=date.today(), source='pl:nbp:table-a', source_currency='EUR',
other_currency='PLN', rate=Decimal('4.2355'))
yield event, o
@@ -349,23 +347,6 @@ def test_invoice_indirect_currency_conversion(env):
assert inv.foreign_currency_source == 'eu:ecb:eurofxref-daily'
@pytest.mark.django_db
def test_invoice_pln_currency_conversion(env):
event, order = env
event.settings.invoice_eu_currencies = 'PLN'
event.settings.set('invoice_language', 'en')
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw',
country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order,
is_business=True)
inv = generate_invoice(order)
assert inv.foreign_currency_display == "PLN"
assert inv.foreign_currency_rate == Decimal("4.2355")
assert inv.foreign_currency_rate_date == date.today()
assert inv.foreign_currency_source == 'pl:nbp:table-a'
@pytest.mark.django_db
def test_invoice_czk_currency_conversion(env):
event, order = env

View File

@@ -747,92 +747,6 @@ class SubEventsTest(SoupTest):
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
def test_create_bulk_skip_existing(self):
with scopes_disabled():
self.event1.subevents.all().delete()
# SubEvent ends at rrule start time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 4, 9, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 4, 10, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent overlaps rrule start
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 5, 9, 30, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 5, 10, 30, tzinfo=datetime.timezone.utc),
)
# SubEvent times are same as rrule
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 6, 10, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 6, 11, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent starts at rrule end time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 7, 11, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 7, 12, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent overlaps entire rrule time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 8, 9, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 8, 12, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent has before rrule time and no end
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 9, 9, 0, tzinfo=datetime.timezone.utc),
)
existing_events = list(self.event1.subevents.values_list('pk', flat=True))
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-end': 'count',
'rruleformset-0-count': '10',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'weekly',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-weekly_byweekday': ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'],
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-monthly_same': 'on',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '12:00:00',
'timeformset-0-time_to': '13:00:00',
'rruleformset-0-until': '2019-04-03',
'skip_if_overlap': 'on',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.exclude(pk__in=existing_events).order_by('date_from'))
assert len(ses) == 7
assert [s.date_from.date().isoformat() for s in ses] == [
'2018-04-03',
'2018-04-04',
'2018-04-07',
'2018-04-09',
'2018-04-10',
'2018-04-11',
'2018-04-12'
]
def test_delete_bulk(self):
self.subevent2.active = True
self.subevent2.save()

View File

@@ -33,7 +33,7 @@ from django.conf import settings
from django.core import mail as djmail
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.signing import dumps
from django.test import Client, TestCase, TransactionTestCase
from django.test import TestCase, TransactionTestCase
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django_countries.fields import Country
@@ -4413,18 +4413,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert len(djmail.outbox) == 1
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
def test_checkout_empty_session_valid_cart(self):
client = Client()
with scopes_disabled():
api_cid = "{}@api".format(get_random_string(48))
CartPosition.objects.create(
event=self.event, cart_id=api_cid, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = client.get('/%s/%s/w/1234567890abcdef/checkout/questions/' % (self.orga.slug, self.event.slug), query_params={"take_cart_id": api_cid})
assert '€23.00' in response.content.decode()
class CheckoutTransactionTestCase(BaseCheckoutTestCase, TransactionTestCase):
def test_order_confirmation_mail_invoice_sent_somewhere_else(self):