diff --git a/src/pretix/base/email.py b/src/pretix/base/email.py index 963d45a507..cf869dddfd 100644 --- a/src/pretix/base/email.py +++ b/src/pretix/base/email.py @@ -222,6 +222,7 @@ class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder): def get_available_placeholders(event, base_parameters): if 'order' in base_parameters: base_parameters.append('invoice_address') + base_parameters.append('position_or_address') params = {} for r, val in register_mail_placeholders.send(sender=event): if not isinstance(val, (list, tuple)): @@ -241,6 +242,8 @@ def get_email_context(**kwargs): kwargs['invoice_address'] = kwargs['order'].invoice_address except InvoiceAddress.DoesNotExist: kwargs['invoice_address'] = InvoiceAddress() + finally: + kwargs.setdefault("position_or_address", kwargs['invoice_address']) ctx = {} for r, val in register_mail_placeholders.send(sender=event): if not isinstance(val, (list, tuple)): @@ -268,7 +271,8 @@ def get_best_name(position_or_address, parts=False): if isinstance(position_or_address, InvoiceAddress): if position_or_address.name: return position_or_address.name_parts if parts else position_or_address.name - position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first() + elif position_or_address.order: + position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first() if isinstance(position_or_address, OrderPosition): if position_or_address.attendee_name: diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 2beda0775f..0cbcf8e563 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -36,8 +36,8 @@ from pretix.base.i18n import language from pretix.base.models import InvoiceAddress, Question, QuestionOption from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix from pretix.base.settings import ( - COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES, - PERSON_NAME_TITLE_GROUPS, + COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS, + PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, ) from pretix.base.templatetags.rich_text import rich_text from pretix.control.forms import ExtFileField, SplitDateTimeField @@ -49,7 +49,7 @@ from pretix.presale.signals import question_form_fields logger = logging.getLogger(__name__) -REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name'] +REQUIRED_NAME_PARTS = ['salutation', 'given_name', 'family_name', 'full_name'] class NamePartsWidget(forms.MultiWidget): @@ -73,6 +73,8 @@ class NamePartsWidget(forms.MultiWidget): a['data-fname'] = fname if fname == 'title' and self.titles: widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]])) + elif fname == 'salutation': + widgets.append(Select(attrs=a, choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS])) else: widgets.append(self.widget(attrs=a)) super().__init__(widgets, attrs) @@ -162,12 +164,18 @@ class NamePartsFormField(forms.MultiValueField): **d, choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]] ) - field.part_name = fname - fields.append(field) + + elif fname == 'salutation': + d = dict(defaults) + d.pop('max_length', None) + field = forms.ChoiceField( + **d, + choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS] + ) else: field = forms.CharField(**defaults) - field.part_name = fname - fields.append(field) + field.part_name = fname + fields.append(field) super().__init__( fields=fields, require_all_fields=False, *args, **kwargs ) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 718bbb0d6c..a798366b17 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1845,7 +1845,7 @@ PERSON_NAME_TITLE_GROUPS = OrderedDict([ 'Mx', 'Dr', 'Professor', - 'Sir' + 'Sir', ))), ('german_common', (_('Most common German titles'), ( 'Dr.', @@ -1853,9 +1853,16 @@ PERSON_NAME_TITLE_GROUPS = OrderedDict([ 'Prof. Dr.', ))) ]) + +PERSON_NAME_SALUTATIONS = [ + pgettext_lazy("person_name_salutation", "Ms"), + pgettext_lazy("person_name_salutation", "Mr"), +] + PERSON_NAME_SCHEMES = OrderedDict([ ('given_family', { 'fields': ( + # field_name, label, weight for widget width ('given_name', _('Given name'), 1), ('family_name', _('Family name'), 1), ), @@ -2010,6 +2017,24 @@ PERSON_NAME_SCHEMES = OrderedDict([ '_scheme': 'full_transcription', }, }), + ('salutation_title_given_family', { + 'fields': ( + ('salutation', pgettext_lazy('person_name', 'Salutation'), 1), + ('title', pgettext_lazy('person_name', 'Title'), 1), + ('given_name', _('Given name'), 2), + ('family_name', _('Family name'), 2), + ), + 'concatenation': lambda d: ' '.join( + str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p + ), + 'sample': { + 'salutation': pgettext_lazy('person_name_sample', 'Mr'), + 'title': pgettext_lazy('person_name_sample', 'Dr'), + 'given_name': pgettext_lazy('person_name_sample', 'John'), + 'family_name': pgettext_lazy('person_name_sample', 'Doe'), + '_scheme': 'title_salutation_given_family', + }, + }), ]) COUNTRIES_WITH_STATE_IN_ADDRESS = { # Source: http://www.bitboost.com/ref/international-address-formats.html @@ -2025,7 +2050,6 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = { 'US': (['State', 'Outlying area', 'District'], 'short'), } - settings_hierarkey = Hierarkey(attribute_name='settings') for k, v in DEFAULTS.items(): @@ -2095,7 +2119,7 @@ class SettingsSandbox: def __delattr__(self, key: str) -> None: del self._event.settings[self._convert_key(key)] - def get(self, key: str, default: Any=None, as_type: type=str): + def get(self, key: str, default: Any = None, as_type: type = str): return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type) def set(self, key: str, value: Any): diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 4d2f98bfbd..8f0919f06d 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -604,7 +604,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): def test_attendee_name_scheme(self): self.event.settings.set('attendee_names_asked', True) self.event.settings.set('attendee_names_required', True) - self.event.settings.set('name_scheme', 'title_given_middle_family') + self.event.settings.set('name_scheme', 'salutation_title_given_family') with scopes_disabled(): cr1 = CartPosition.objects.create( event=self.event, cart_id=self.session_key, item=self.ticket, @@ -612,17 +612,16 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): ) response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") - self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_0"]' % cr1.id)), 1) + self.assertEqual(len(doc.select('select[name="%s-attendee_name_parts_0"]' % cr1.id)), 1) self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_1"]' % cr1.id)), 1) self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_2"]' % cr1.id)), 1) self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_3"]' % cr1.id)), 1) - # Not all required fields filled out, expect failure response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { '%s-attendee_name_parts_0' % cr1.id: 'Mr', - '%s-attendee_name_parts_1' % cr1.id: 'John', - '%s-attendee_name_parts_2' % cr1.id: 'F', - '%s-attendee_name_parts_3' % cr1.id: 'Kennedy', + '%s-attendee_name_parts_1' % cr1.id: '', + '%s-attendee_name_parts_2' % cr1.id: 'John', + '%s-attendee_name_parts_3' % cr1.id: 'Doe', 'email': 'admin@localhost' }) self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), @@ -630,13 +629,13 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): with scopes_disabled(): cr1 = CartPosition.objects.get(id=cr1.id) - self.assertEqual(cr1.attendee_name, 'Mr John F Kennedy') + self.assertEqual(cr1.attendee_name, 'John Doe') self.assertEqual(cr1.attendee_name_parts, { + 'salutation': 'Mr', + 'title': '', 'given_name': 'John', - 'title': 'Mr', - 'middle_name': 'F', - 'family_name': 'Kennedy', - "_scheme": "title_given_middle_family" + 'family_name': 'Doe', + "_scheme": "salutation_title_given_family" }) def test_attendee_name_optional(self):