mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Add company and address fields to attendees (#1633)
* Add company and address fields to attendees * Update src/pretix/control/templates/pretixcontrol/event/settings.html Co-Authored-By: Martin Gross <gross@rami.io> Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -558,6 +558,10 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'confirm_text',
|
||||
'order_email_asked_twice',
|
||||
'payment_term_days',
|
||||
|
||||
@@ -39,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
else:
|
||||
elif hasattr(instance, 'country_old'):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
@@ -211,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
@@ -516,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
max_digits=10)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in self.fields.items():
|
||||
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||
v.required = False
|
||||
v.allow_blank = True
|
||||
v.allow_null = True
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
raise ValidationError(
|
||||
@@ -576,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Attendee name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Attendee email'),
|
||||
_('Company'),
|
||||
_('Address'),
|
||||
_('ZIP code'),
|
||||
_('City'),
|
||||
_('Country'),
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Pseudonymization ID'),
|
||||
]
|
||||
@@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
)
|
||||
row += [
|
||||
op.attendee_email,
|
||||
op.company or '',
|
||||
op.street or '',
|
||||
op.zipcode or '',
|
||||
op.city or '',
|
||||
op.country if op.country else '',
|
||||
op.state or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
]
|
||||
|
||||
@@ -245,7 +245,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
if item.admission and event.settings.attendee_names_asked:
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required,
|
||||
required=event.settings.attendee_names_required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=_('Attendee name'),
|
||||
@@ -253,7 +253,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
)
|
||||
if item.admission and event.settings.attendee_emails_asked:
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required,
|
||||
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||
widget=forms.EmailInput(
|
||||
@@ -262,6 +262,73 @@ class BaseQuestionsForm(forms.Form):
|
||||
}
|
||||
)
|
||||
)
|
||||
if item.admission and event.settings.attendee_company_asked:
|
||||
self.fields['company'] = forms.CharField(
|
||||
required=event.settings.attendee_company_required and not self.all_optional,
|
||||
label=_('Company'),
|
||||
initial=(cartpos.company if cartpos else orderpos.company),
|
||||
)
|
||||
|
||||
if item.admission and event.settings.attendee_addresses_asked:
|
||||
self.fields['street'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Address'),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
initial=(cartpos.street if cartpos else orderpos.street),
|
||||
)
|
||||
self.fields['zipcode'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('ZIP code'),
|
||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
)
|
||||
self.fields['city'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('City'),
|
||||
initial=(cartpos.city if cartpos else orderpos.city),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||
self.fields['country'] = CountryField().formfield(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Country'),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
}),
|
||||
)
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
cc = str(country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
self.fields['state'].widget.is_required = True
|
||||
|
||||
for q in questions:
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
@@ -423,6 +490,10 @@ class BaseQuestionsForm(forms.Form):
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
|
||||
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-01 11:24
|
||||
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0149_order_cancellation_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='city',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='company',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='street',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='zipcode',
|
||||
field=models.CharField(max_length=30, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='city',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='company',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='street',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='zipcode',
|
||||
field=models.CharField(max_length=30, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1065,6 +1065,13 @@ class AbstractPosition(models.Model):
|
||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
|
||||
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
|
||||
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
|
||||
position.attendee_email = value
|
||||
|
||||
|
||||
class AttendeeCompany(ImportColumn):
|
||||
identifier = 'attendee_company'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Company')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.company = value or ''
|
||||
|
||||
|
||||
class AttendeeStreet(ImportColumn):
|
||||
identifier = 'attendee_street'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Address')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.address = value or ''
|
||||
|
||||
|
||||
class AttendeeZip(ImportColumn):
|
||||
identifier = 'attendee_zipcode'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('ZIP code')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.zipcode = value or ''
|
||||
|
||||
|
||||
class AttendeeCity(ImportColumn):
|
||||
identifier = 'attendee_city'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('City')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.city = value or ''
|
||||
|
||||
|
||||
class AttendeeCountry(ImportColumn):
|
||||
identifier = 'attendee_country'
|
||||
default_value = None
|
||||
|
||||
@property
|
||||
def initial(self):
|
||||
return 'static:' + str(guess_country(self.event))
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Country')
|
||||
|
||||
def static_choices(self):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not Country(value).numeric:
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.country = value or ''
|
||||
|
||||
|
||||
class AttendeeState(ImportColumn):
|
||||
identifier = 'attendee_state'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('State')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(_("States are not supported for this country."))
|
||||
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
|
||||
match = [
|
||||
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
|
||||
if s.type in types and (s.code[3:] == value or s.name == value)
|
||||
]
|
||||
if len(match) == 0:
|
||||
raise ValidationError(_("Please enter a valid state."))
|
||||
return match[0].code[3:]
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.state = value or ''
|
||||
|
||||
|
||||
class Price(ImportColumn):
|
||||
identifier = 'price'
|
||||
verbose_name = gettext_lazy('Price')
|
||||
@@ -596,6 +689,12 @@ def get_all_columns(event):
|
||||
default.append(AttendeeNamePart(event, n, l))
|
||||
default += [
|
||||
AttendeeEmail(event),
|
||||
AttendeeCompany(event),
|
||||
AttendeeStreet(event),
|
||||
AttendeeZip(event),
|
||||
AttendeeCity(event),
|
||||
AttendeeCountry(event),
|
||||
AttendeeState(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
Locale(event),
|
||||
|
||||
@@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Sample city"),
|
||||
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
|
||||
}),
|
||||
("attendee_company", {
|
||||
"label": _("Attendee company"),
|
||||
"editor_sample": _("Sample company"),
|
||||
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
|
||||
}),
|
||||
("addons", {
|
||||
"label": _("List of Add-Ons"),
|
||||
"editor_sample": _("Addon 1\nAddon 2"),
|
||||
|
||||
@@ -105,6 +105,44 @@ DEFAULTS = {
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_company_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for company per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_company_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require company per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for postal addresses per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require postal addresses per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
|
||||
)
|
||||
},
|
||||
'order_email_asked_twice': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
|
||||
@@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder):
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
class AttendeeNameShredder(BaseDataShredder):
|
||||
verbose_name = _('Attendee names')
|
||||
identifier = 'attendee_names'
|
||||
description = _('This will remove all attendee names from order positions, as well as logged changes to them.')
|
||||
class AttendeeInfoShredder(BaseDataShredder):
|
||||
verbose_name = _('Attendee info')
|
||||
identifier = 'attendee_info'
|
||||
description = _('This will remove all attendee names and postal addresses from order positions, as well as logged '
|
||||
'changes to them.')
|
||||
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
yield 'attendee-names.json', 'application/json', json.dumps({
|
||||
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
|
||||
for op in OrderPosition.all.filter(
|
||||
yield 'attendee-info.json', 'application/json', json.dumps({
|
||||
'{}-{}'.format(op.order.code, op.positionid): {
|
||||
'name': op.attendee_name,
|
||||
'company': op.company,
|
||||
'street': op.street,
|
||||
'zipcode': op.zipcode,
|
||||
'city': op.city,
|
||||
'country': str(op.country) if op.country else None,
|
||||
'state': op.state
|
||||
} for op in OrderPosition.all.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
@@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
OrderPosition.all.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
|
||||
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
|
||||
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
|
||||
zipcode=None, city=None)
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
@@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
d['data'][i]['attendee_name_parts'] = {
|
||||
'_legacy': '█'
|
||||
}
|
||||
if 'company' in row:
|
||||
d['data'][i]['company'] = '█'
|
||||
if 'street' in row:
|
||||
d['data'][i]['street'] = '█'
|
||||
if 'zipcode' in row:
|
||||
d['data'][i]['zipcode'] = '█'
|
||||
if 'city' in row:
|
||||
d['data'][i]['city'] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
@@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder):
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
return [
|
||||
EmailAddressShredder,
|
||||
AttendeeNameShredder,
|
||||
AttendeeInfoShredder,
|
||||
InvoiceAddressShredder,
|
||||
QuestionAnswerShredder,
|
||||
InvoiceShredder,
|
||||
|
||||
@@ -85,10 +85,20 @@ class BaseQuestionsViewMixin:
|
||||
for k, v in form.cleaned_data.items():
|
||||
if k == 'attendee_name_parts':
|
||||
form.pos.attendee_name_parts = v if v else None
|
||||
form.pos.save()
|
||||
elif k == 'attendee_email':
|
||||
form.pos.attendee_email = v if v != '' else None
|
||||
form.pos.save()
|
||||
elif k == 'company':
|
||||
form.pos.company = v if v != '' else None
|
||||
elif k == 'street':
|
||||
form.pos.street = v if v != '' else None
|
||||
elif k == 'zipcode':
|
||||
form.pos.zipcode = v if v != '' else None
|
||||
elif k == 'city':
|
||||
form.pos.city = v if v != '' else None
|
||||
elif k == 'country':
|
||||
form.pos.country = v if v != '' else None
|
||||
elif k == 'state':
|
||||
form.pos.state = v if v != '' else None
|
||||
elif k.startswith('question_'):
|
||||
field = form.fields[k]
|
||||
if hasattr(field, 'answer'):
|
||||
@@ -119,7 +129,7 @@ class BaseQuestionsViewMixin:
|
||||
meta_info['question_form_data'][k] = v
|
||||
|
||||
form.pos.meta_info = json.dumps(meta_info)
|
||||
form.pos.save(update_fields=['meta_info'])
|
||||
form.pos.save()
|
||||
return not failed
|
||||
|
||||
def _save_to_answer(self, field, answer, value):
|
||||
|
||||
@@ -515,6 +515,10 @@ class EventSettingsForm(SettingsForm):
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'confirm_text',
|
||||
'order_email_asked_twice',
|
||||
'last_order_modification_date',
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_company_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_company_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_addresses_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_addresses_required layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Texts" %}</legend>
|
||||
|
||||
@@ -388,6 +388,29 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.admission and event.settings.attendee_company_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.admission and event.settings.attendee_addresses_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|linebreaksbr }}<br>
|
||||
{{ line.zipcode }} {{ line.city }}<br>
|
||||
{{ line.country.name }}
|
||||
{% if line.state %}<br>{{ line.state }}{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>
|
||||
{{ q.question }}
|
||||
|
||||
@@ -428,7 +428,7 @@ class CSVCheckinList(CheckInListMixin, ListExporter):
|
||||
for q in questions:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
row.append(ia.company)
|
||||
row.append(op.company or ia.company)
|
||||
row.append(op.voucher.code if op.voucher else "")
|
||||
row.append(op.order.datetime.astimezone(self.event.timezone).strftime('%Y-%m-%d'))
|
||||
row.append(_('Yes') if op.order.checkin_attention or op.item.checkin_attention else _('No'))
|
||||
|
||||
@@ -488,6 +488,16 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.admission and self.request.event.settings.get('attendee_company_required', as_type=bool) \
|
||||
and cp.company is None:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.admission and self.request.event.settings.get('attendee_attendees_required', as_type=bool) \
|
||||
and (cp.street is None or cp.city is None or cp.country is None):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
|
||||
responses = question_form_fields.send(sender=self.request.event, position=cp)
|
||||
form_data = cp.meta_info_data.get('question_form_data', {})
|
||||
|
||||
@@ -44,12 +44,51 @@
|
||||
{% if line.has_questions %}
|
||||
<dl>
|
||||
{% if line.item.admission and event.settings.attendee_names_asked %}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
<dt class="sr-only">
|
||||
{% trans "Attendee name" %}
|
||||
</dt>
|
||||
<dd class="toplevel">
|
||||
<span data-toggle="tooltip" title="{% trans "Attendee name" %}">
|
||||
{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</span>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.admission and event.settings.attendee_emails_asked %}
|
||||
<dt>{% trans "Attendee email" %}</dt>
|
||||
<dd>{% if line.attendee_email %}{{ line.attendee_email }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
<dt class="sr-only">
|
||||
{% trans "Attendee email" %}
|
||||
</dt>
|
||||
<dd class="toplevel">
|
||||
<span data-toggle="tooltip" title="{% trans "Attendee email" %}">
|
||||
{% if line.attendee_email %}{{ line.attendee_email }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</span>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.admission and event.settings.attendee_company_asked %}
|
||||
<dt class="sr-only">
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
<dd class="toplevel">
|
||||
<span data-toggle="tooltip" title="{% trans "Attendee company" %}">
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</span>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.admission and event.settings.attendee_addresses_asked %}
|
||||
<dt class="sr-only">
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
<dd class="toplevel">
|
||||
<span data-toggle="tooltip" title="{% trans "Attendee address" %}">
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|linebreaksbr }}<br>
|
||||
{{ line.zipcode }} {{ line.city }}<br>
|
||||
{{ line.country.name }}
|
||||
{% if line.state %}<br>{{ line.state }}{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</span>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>{{ q.question }}</dt>
|
||||
|
||||
@@ -167,22 +167,35 @@ $(function () {
|
||||
return false;
|
||||
});
|
||||
var copy_to_first_ticket = true;
|
||||
$("input[id*=attendee_name_parts_], input[id*=attendee_email]").each(function () {
|
||||
var attendee_address_fields = $("input[id*=attendee_name_parts_], input[id*=attendee_email], .questions-form" +
|
||||
" input[id$=company], .questions-form[id$=street], .questions-form input[id$=zipcode], .questions-form" +
|
||||
" input[id$=city]");
|
||||
attendee_address_fields.each(function () {
|
||||
if ($(this).val()) {
|
||||
copy_to_first_ticket = false;
|
||||
}
|
||||
})
|
||||
$("input[id^=id_name_parts_], #id_email").change(function () {
|
||||
$("select[id^=id_name_parts], input[id^=id_name_parts_], #id_email, #id_street, #id_company, #id_zipcode," +
|
||||
" #id_city, #id_country, #id_state").change(function () {
|
||||
if (copy_to_first_ticket) {
|
||||
$(".questions-form").first().find("input[id*=attendee_email]").val($("#id_email").val());
|
||||
$(".questions-form").first().find("input[id*=attendee_name_parts]").each(function () {
|
||||
$(".questions-form").first().find("input[id$=company]").val($("#id_company").val());
|
||||
$(".questions-form").first().find("textarea[id$=street]").val($("#id_street").val());
|
||||
$(".questions-form").first().find("input[id$=zipcode]").val($("#id_zipcode").val());
|
||||
$(".questions-form").first().find("input[id$=city]").val($("#id_city").val());
|
||||
|
||||
$(".questions-form").first().find("select[id$=state]").val($("#id_state").val());
|
||||
if ($(".questions-form").first().find("select[id$=country]").val() !== $("#id_country").val()) {
|
||||
$(".questions-form").first().find("select[id$=country]").val($("#id_country").val()).trigger('change');
|
||||
}
|
||||
$(".questions-form").first().find("[id*=attendee_name_parts]").each(function () {
|
||||
var parts = $(this).attr("id").split("_");
|
||||
var num = parts[parts.length - 1];
|
||||
$(this).val($("#id_name_parts_" + num).val());
|
||||
});
|
||||
}
|
||||
});
|
||||
$("input[id*=attendee_name_parts_], input[id*=attendee_email]").change(function () {
|
||||
attendee_address_fields.change(function () {
|
||||
copy_to_first_ticket = false;
|
||||
});
|
||||
|
||||
@@ -273,7 +286,7 @@ $(function () {
|
||||
$("select[name$=state]").each(function () {
|
||||
var dependent = $(this),
|
||||
counter = 0,
|
||||
dependency = $(this).closest("form").find('select[name$=country]'),
|
||||
dependency = $(this).closest(".panel-body, form").find('select[name$=country]'),
|
||||
update = function (ev) {
|
||||
counter++;
|
||||
var curCounter = counter;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
padding-left: 20px;
|
||||
margin-bottom: 0;
|
||||
|
||||
dd {
|
||||
dd:not(.toplevel) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,12 @@ TEST_ORDERPOSITION_RES = {
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"seat": None,
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
@@ -1503,6 +1509,7 @@ ORDER_CREATE_PAYLOAD = {
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"company": "FOOCORP",
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
@@ -1556,6 +1563,7 @@ def test_order_create(token_client, organizer, event, item, quota, question):
|
||||
assert pos.item == item
|
||||
assert pos.price == Decimal("23.00")
|
||||
assert pos.attendee_name_parts == {"full_name": "Peter", "_scheme": "full"}
|
||||
assert pos.company == "FOOCORP"
|
||||
with scopes_disabled():
|
||||
answ = pos.answers.first()
|
||||
assert answ.question == question
|
||||
@@ -1655,6 +1663,12 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
|
||||
'tax_rule': None,
|
||||
'pseudonymization_id': 'PREVIEW',
|
||||
'seat': None,
|
||||
'company': "FOOCORP",
|
||||
'street': None,
|
||||
'city': None,
|
||||
'zipcode': None,
|
||||
'state': None,
|
||||
'country': None,
|
||||
'canceled': False
|
||||
}
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@ from pretix.base.models import (
|
||||
from pretix.base.services.invoices import generate_invoice, invoice_pdf_task
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.shredder import (
|
||||
AttendeeNameShredder, CachedTicketShredder, EmailAddressShredder,
|
||||
AttendeeInfoShredder, CachedTicketShredder, EmailAddressShredder,
|
||||
InvoiceAddressShredder, InvoiceShredder, PaymentInfoShredder,
|
||||
QuestionAnswerShredder, WaitingListShredder, shred_constraints,
|
||||
)
|
||||
@@ -57,7 +57,8 @@ def order(event, item):
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
|
||||
attendee_email="foo@example.org"
|
||||
attendee_email="foo@example.org",
|
||||
company='Foobar',
|
||||
)
|
||||
return o
|
||||
|
||||
@@ -145,15 +146,23 @@ def test_attendee_name_shredder(event, order):
|
||||
l1 = order.log_action(
|
||||
'pretix.event.order.modified',
|
||||
data={
|
||||
"data": [{"attendee_name": "Hans", "question_1": "Test"}],
|
||||
"data": [{"attendee_name": "Peter", "question_1": "Test", "company": "Foobar"}],
|
||||
"invoice_data": {"name": "Foo"}
|
||||
}
|
||||
)
|
||||
|
||||
s = AttendeeNameShredder(event)
|
||||
s = AttendeeInfoShredder(event)
|
||||
f = list(s.generate_files())
|
||||
assert json.loads(f[0][2]) == {
|
||||
'{}-{}'.format(order.code, 1): 'Peter'
|
||||
'{}-{}'.format(order.code, 1): {
|
||||
'name': 'Peter',
|
||||
'company': 'Foobar',
|
||||
'street': None,
|
||||
'zipcode': None,
|
||||
'city': None,
|
||||
'country': None,
|
||||
'state': None
|
||||
}
|
||||
}
|
||||
s.shred_data()
|
||||
order.refresh_from_db()
|
||||
@@ -161,6 +170,7 @@ def test_attendee_name_shredder(event, order):
|
||||
l1.refresh_from_db()
|
||||
assert 'Hans' not in l1.data
|
||||
assert 'Foo' in l1.data
|
||||
assert 'Foobar' not in l1.data
|
||||
assert 'Test' in l1.data
|
||||
|
||||
|
||||
|
||||
@@ -139,15 +139,18 @@ def test_csv_order_by_inherited_name_parts(event): # noqa
|
||||
order=order2,
|
||||
item=event.items.first(),
|
||||
variation=None,
|
||||
company='BARCORP',
|
||||
price=Decimal("23"),
|
||||
secret='hutjztuxhkbtwnesv2suqv26k6ttytyy'
|
||||
)
|
||||
InvoiceAddress.objects.create(
|
||||
order=event.orders.get(code='BAR'),
|
||||
company='FOOCORP',
|
||||
name_parts={"title": "Mr", "given_name": "Albert", "middle_name": "J", "family_name": "Zulu", "_scheme": "title_given_middle_family"}
|
||||
)
|
||||
InvoiceAddress.objects.create(
|
||||
order=event.orders.get(code='FOO'),
|
||||
company='FOOCORP',
|
||||
name_parts={"title": "Mr", "given_name": "Paul", "middle_name": "A", "family_name": "Jones", "_scheme": "title_given_middle_family"}
|
||||
)
|
||||
|
||||
@@ -164,9 +167,9 @@ def test_csv_order_by_inherited_name_parts(event): # noqa
|
||||
"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special
|
||||
attention","Comment"
|
||||
"BAR","Mr Albert J Zulu","Mr","Albert","J","Zulu","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytyy",
|
||||
"dummy@dummy.test","","","2019-02-22","No",""
|
||||
"dummy@dummy.test","BARCORP","","2019-02-22","No",""
|
||||
"FOO","Mr Paul A Jones","Mr","Paul","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx",
|
||||
"dummy@dummy.test","","","2019-02-22","No",""
|
||||
"dummy@dummy.test","FOOCORP","","2019-02-22","No",""
|
||||
""")
|
||||
c = CSVCheckinList(event)
|
||||
_, _, content = c.render({
|
||||
|
||||
@@ -436,6 +436,81 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.attendee_email, 'foo@localhost')
|
||||
|
||||
def test_attendee_company_required(self):
|
||||
self.event.settings.set('attendee_company_asked', True)
|
||||
self.event.settings.set('attendee_company_required', True)
|
||||
with scopes_disabled():
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
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-company"]' % 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-company' % cr1.id: '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-company' % cr1.id: 'foobar',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
with scopes_disabled():
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.company, 'foobar')
|
||||
|
||||
def test_attendee_address_required(self):
|
||||
self.event.settings.set('attendee_addresses_asked', True)
|
||||
self.event.settings.set('attendee_addresses_required', True)
|
||||
with scopes_disabled():
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
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('textarea[name="%s-street"]' % 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-street' % cr1.id: '',
|
||||
'%s-zipcode' % cr1.id: '',
|
||||
'%s-city' % cr1.id: '',
|
||||
'%s-country' % cr1.id: '',
|
||||
'%s-state' % cr1.id: '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-street' % cr1.id: 'Musterstrasse',
|
||||
'%s-zipcode' % cr1.id: '12345',
|
||||
'%s-city' % cr1.id: 'Musterstadt',
|
||||
'%s-country' % cr1.id: 'DE',
|
||||
'%s-state' % cr1.id: '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
with scopes_disabled():
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
self.assertEqual(cr1.street, 'Musterstrasse')
|
||||
self.assertEqual(cr1.zipcode, '12345')
|
||||
self.assertEqual(cr1.city, 'Musterstadt')
|
||||
self.assertEqual(cr1.country, 'DE')
|
||||
|
||||
def test_attendee_name_required(self):
|
||||
self.event.settings.set('attendee_names_asked', True)
|
||||
self.event.settings.set('attendee_names_required', True)
|
||||
|
||||
Reference in New Issue
Block a user