mirror of
https://github.com/pretix/pretix.git
synced 2026-05-16 17:03:58 +00:00
Merge pull request 'upstream/2025.6.0' (#6) from upstream/2025.6.0 into master
Reviewed-on: simon/pretix_cgo#6
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user