Merge pull request 'upstream/2025.6.0' (#6) from upstream/2025.6.0 into master
All checks were successful
Build Deploy email notification tool / Apply-Kubernetes-Resources (push) Successful in 1m41s

Reviewed-on: simon/pretix_cgo#6
This commit is contained in:
2025-07-04 20:24:59 +00:00
188 changed files with 56803 additions and 58908 deletions

View File

@@ -182,10 +182,15 @@ class NamePartsWidget(forms.MultiWidget):
if self.field.required:
these_attrs['required'] = 'required'
these_attrs.pop('data-no-required-attr', None)
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
autofill_section = self.attrs.get('autocomplete', '')
autofill_by_name_scheme = self.autofill_map.get(self.scheme['fields'][i][0], 'off')
if autofill_by_name_scheme == "off" or autofill_section.strip() == "off":
these_attrs['autocomplete'] = "off"
else:
these_attrs['autocomplete'] = (autofill_section + ' ' + autofill_by_name_scheme).strip()
these_attrs['data-size'] = self.scheme['fields'][i][2]
if len(self.widgets) > 1:
these_attrs['aria-label'] = self.scheme['fields'][i][1]
these_attrs['aria-label'] = self.scheme['fields'][i][1]
else:
these_attrs = final_attrs
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
@@ -303,7 +308,10 @@ class WrappedPhonePrefixSelect(Select):
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
})
def render(self, name, value, *args, **kwargs):
return super().render(name, value or self.initial, *args, **kwargs)
@@ -326,11 +334,11 @@ class WrappedPhonePrefixSelect(Select):
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def __init__(self, attrs=None, initial=None):
attrs = {
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)')
}
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs=attrs))
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs={
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)'),
'autocomplete': 'tel-national',
}))
super(PhoneNumberPrefixWidget, self).__init__(widgets)
def render(self, name, value, attrs=None, renderer=None):
output = super().render(name, value, attrs, renderer)
@@ -888,10 +896,17 @@ class BaseQuestionsForm(forms.Form):
'Please enter a date no later than {max}.',
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
)
if initial and initial.answer:
try:
_initial = dateutil.parser.parse(initial.answer).date()
except dateutil.parser.ParserError:
_initial = None
else:
_initial = None
field = forms.DateField(
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
initial=_initial,
widget=DatePickerWidget(attrs),
)
if q.valid_date_min:
@@ -899,10 +914,17 @@ class BaseQuestionsForm(forms.Form):
if q.valid_date_max:
field.validators.append(MaxDateValidator(q.valid_date_max))
elif q.type == Question.TYPE_TIME:
if initial and initial.answer:
try:
_initial = dateutil.parser.parse(initial.answer).time()
except dateutil.parser.ParserError:
_initial = None
else:
_initial = None
field = forms.TimeField(
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
initial=_initial,
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
@@ -923,10 +945,19 @@ class BaseQuestionsForm(forms.Form):
'Please enter a date and time no later than {max}.',
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
)
if initial and initial.answer:
try:
_initial = dateutil.parser.parse(initial.answer).astimezone(tz)
except dateutil.parser.ParserError:
_initial = None
else:
_initial = None
field = SplitDateTimeField(
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
initial=_initial,
widget=SplitDateTimePickerWidget(
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=q.valid_datetime_min,
@@ -987,8 +1018,19 @@ class BaseQuestionsForm(forms.Form):
value.initial = data.get('question_form_data', {}).get(key)
for k, v in self.fields.items():
if isinstance(v.widget, forms.MultiWidget):
for w in v.widget.widgets:
autocomplete = w.attrs.get('autocomplete', '')
if autocomplete.strip() == "off":
w.attrs['autocomplete'] = 'off'
else:
w.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
autocomplete = v.widget.attrs.get('autocomplete', '')
if autocomplete.strip() == "off":
v.widget.attrs['autocomplete'] = 'off'
else:
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
def clean(self):
from pretix.base.addressvalidation import \
@@ -1202,7 +1244,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
autocomplete = v.widget.attrs.get('autocomplete', '')
if autocomplete.strip() == "off":
v.widget.attrs['autocomplete'] = 'off'
else:
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + autocomplete
def clean(self):
from pretix.base.addressvalidation import \

View File

@@ -26,7 +26,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import (
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
Voucher,
Voucher, WaitingListEntry,
)
from .logentrytype_registry import ( # noqa
@@ -145,3 +145,15 @@ class TaxRuleLogEntryType(EventLogEntryType):
object_link_viewname = 'control:event.settings.tax.edit'
object_link_argname = 'rule'
content_type = TaxRule
class WaitingListEntryLogEntryType(EventLogEntryType):
object_link_wrapper = '{val}'
object_link_viewname = 'control:event.orders.waitinglist'
content_type = WaitingListEntry
def get_object_link_info(self, logentry) -> Optional[dict]:
info = super().get_object_link_info(logentry)
if info and 'href' in info:
info['href'] += '?status=a&entry=' + str(logentry.content_object.pk)
return info

View File

@@ -1084,6 +1084,7 @@ class Event(EventMixin, LoggedModel):
s.product = item_map[s.product_id]
s.save(force_insert=True)
valid_sales_channel_identifers = set(self.organizer.sales_channels.values_list("identifier", flat=True))
skip_settings = (
'ticket_secrets_pretix_sig1_pubkey',
'ticket_secrets_pretix_sig1_privkey',
@@ -1119,6 +1120,11 @@ class Event(EventMixin, LoggedModel):
settings_to_save.append(s)
except ValueError:
pass
elif s.key.startswith('payment_') and s.key.endswith('__restrict_to_sales_channels'):
data = other.settings._unserialize(s.value, as_type=list)
data = [ident for ident in data if ident in valid_sales_channel_identifers]
s.value = other.settings._serialize(data)
settings_to_save.append(s)
else:
settings_to_save.append(s)
other.settings._objects.bulk_create(settings_to_save)

View File

@@ -793,7 +793,7 @@ class Item(LoggedModel):
class Meta:
verbose_name = _("Product")
verbose_name_plural = _("Products")
ordering = ("category__position", "category", "position")
ordering = ("category__position", "category", "position", "pk")
def __str__(self):
return str(self.internal_name or self.name)
@@ -1925,6 +1925,25 @@ class Question(LoggedModel):
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
super().clean()
def clean_type_change(self, old_type, new_type):
if old_type == new_type:
return True
if not self.pk or not self.answers.exists():
return True
if new_type == self.TYPE_TEXT and old_type != self.TYPE_FILE:
# All types can be converted to text except file
return True
if new_type == self.TYPE_STRING and old_type not in (self.TYPE_TEXT, self.TYPE_FILE):
# All types can be converted to string except text or file
return True
if new_type == self.TYPE_CHOICE_MULTIPLE and old_type == self.TYPE_CHOICE:
# Single-choice can be converted to multiple choice without loss
return True
raise ValidationError(
_("The system already contains answers to this question that are not compatible with changing the "
"type of question without data loss. Consider hiding this question and creating a new one instead.")
)
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)

View File

@@ -218,7 +218,6 @@ class WaitingListEntry(LoggedModel):
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth)
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
self.voucher = v
self.save()
@@ -234,6 +233,7 @@ class WaitingListEntry(LoggedModel):
),
user=user,
auth=auth,
log_entry_type='pretix.event.orders.waitinglist.voucher_assigned',
)
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],

View File

@@ -689,11 +689,6 @@ class BasePaymentProvider:
the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
:param total: The total value without the payment method fee, after taxes.
.. versionchanged:: 1.17.0
The ``total`` parameter has been added. For backwards compatibility, this method is called again
without this parameter if it raises a ``TypeError`` on first try.
"""
timing = self._is_available_by_time(cart_id=get_or_create_cart_id(request))
pricing = True

View File

@@ -96,12 +96,19 @@ class SendMailException(Exception):
def clean_sender_name(sender_name: str) -> str:
# Even though we try to properly escape sender names, some characters seem to cause problems when the escaping
# fails due to some forwardings, etc.
# Emails with @ in their sender name are rejected by some mailservers (e.g. Microsoft) because it looks like
# a phishing attempt.
sender_name = sender_name.replace("@", " ")
# Emails with : in their sender name are treated by Microsoft like emails with no From header at all, leading
# to a higher spam likelihood.
sender_name = sender_name.replace(":", " ")
# Emails with , in their sender name look like multiple senders
sender_name = sender_name.replace(",", "")
# Emails with " in their sender name could be escaped, but somehow create issues in reality
sender_name = sender_name.replace("\"", "")
# Emails with excessively long sender names are rejected by some mailservers
if len(sender_name) > 75:

View File

@@ -3749,7 +3749,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'JP': (['Prefecture'], 'long'),
'MY': (['State', 'Federal territory'], 'long'),
'MX': (['State', 'Federal district'], 'short'),
'MX': (['State', 'Federal district', 'Federal entity'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
'Free municipal consortium', 'Decentralized regional entity'], 'short'),

View File

@@ -58,7 +58,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent,
datetime=ev.date_from,
description=pgettext_lazy('timeline', 'Your event starts'),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_date_from_0'
))
if ev.date_to:
@@ -66,7 +66,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent,
datetime=ev.date_to,
description=pgettext_lazy('timeline', 'Your event ends'),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_date_to_0'
))
if ev.date_admission:
@@ -74,7 +74,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent,
datetime=ev.date_admission,
description=pgettext_lazy('timeline', 'Admissions for your event start'),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_date_admission_0'
))
if ev.presale_start:
@@ -82,7 +82,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent,
datetime=ev.presale_start,
description=pgettext_lazy('timeline', 'Start of ticket sales'),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_presale_start_0'
))
tl.append(TimelineEvent(
@@ -95,7 +95,7 @@ def timeline_for_event(event, subevent=None):
pgettext_lazy('timeline', 'End of ticket sales'),
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured') if not ev.presale_end else ""
),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_presale_end_0'
))
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
@@ -104,7 +104,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
edit_url=ev_edit_url
edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0'
))
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
@@ -281,7 +281,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
}) + '#id_available_from_0'
))
if p.available_until:
tl.append(TimelineEvent(
@@ -292,7 +292,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
}) + '#id_available_until_0'
))
for v in ItemVariation.objects.filter(
@@ -311,7 +311,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug,
'organizer': event.organizer.slug,
'item': v.item.pk,
})
}) + '#tab-0-3-open'
))
if v.available_until:
tl.append(TimelineEvent(
@@ -325,7 +325,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug,
'organizer': event.organizer.slug,
'item': v.item.pk,
})
}) + '#tab-0-3-open'
))
pprovs = event.get_payment_providers()
@@ -339,6 +339,24 @@ def timeline_for_event(event, subevent=None):
continue
except:
pass
availability_start = pprov.settings.get('_availability_start', as_type=RelativeDateWrapper)
if availability_start:
d = make_aware(datetime.combine(
availability_start.date(ev),
time(hour=0, minute=0, second=0)
), event.timezone)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=d,
description=pgettext_lazy('timeline', 'Payment provider "{name}" becomes active').format(
name=str(pprov.verbose_name)
),
edit_url=reverse('control:event.settings.payment.provider', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'provider': pprov.identifier,
})
))
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
d = make_aware(datetime.combine(