diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 4fee39b3b..f8044e00f 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -26,7 +26,7 @@ Frontend -------- .. automodule:: pretix.presale.signals - :members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content + :members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional .. automodule:: pretix.presale.signals diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index eebbb9bd7..ab0ea17e1 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -291,17 +291,18 @@ class BaseInvoiceAddressForm(forms.ModelForm): self.event = event = kwargs.pop('event') self.request = kwargs.pop('request', None) self.validate_vat_id = kwargs.pop('validate_vat_id') + self.all_optional = kwargs.pop('all_optional', False) super().__init__(*args, **kwargs) if not event.settings.invoice_address_vatid: del self.fields['vat_id'] - if not event.settings.invoice_address_required: + if not event.settings.invoice_address_required or self.all_optional: for k, f in self.fields.items(): f.required = False f.widget.is_required = False if 'required' in f.widget.attrs: del f.widget.attrs['required'] - elif event.settings.invoice_address_company_required: + elif event.settings.invoice_address_company_required and not self.all_optional: self.initial['is_business'] = True self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True) @@ -314,12 +315,12 @@ class BaseInvoiceAddressForm(forms.ModelForm): self.fields['name_parts'] = NamePartsFormField( max_length=255, - required=event.settings.invoice_name_required, + required=event.settings.invoice_name_required and not self.all_optional, scheme=event.settings.name_scheme, label=_('Name'), initial=(self.instance.name_parts if self.instance else self.instance.name_parts), ) - if event.settings.invoice_address_required and not event.settings.invoice_address_company_required: + if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional: self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0' self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1' self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1' diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 0b3954a9e..77635d865 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -23,8 +23,8 @@ from pretix.presale.forms.checkout import ( AddOnsForm, ContactForm, InvoiceAddressForm, InvoiceNameForm, ) from pretix.presale.signals import ( - checkout_confirm_messages, checkout_flow_steps, contact_form_fields, - order_meta_from_request, question_form_fields, + checkout_all_optional, checkout_confirm_messages, checkout_flow_steps, + contact_form_fields, order_meta_from_request, question_form_fields, ) from pretix.presale.views import CartMixin, get_cart, get_cart_total from pretix.presale.views.cart import ( @@ -308,6 +308,13 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): def is_applicable(self, request): return True + @cached_property + def all_optional(self): + for recv, resp in checkout_all_optional.send(sender=self.request.event, request=self.request): + if resp: + return True + return False + @cached_property def contact_form(self): initial = { @@ -320,7 +327,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): return ContactForm(data=self.request.POST if self.request.method == "POST" else None, event=self.request.event, request=self.request, - initial=initial) + initial=initial, all_optional=self.all_optional) @cached_property def eu_reverse_charge_relevant(self): @@ -348,13 +355,13 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): request=self.request, instance=self.invoice_address, initial=initial, - validate_vat_id=False) + validate_vat_id=False, all_optional=self.all_optional) return InvoiceAddressForm(data=self.request.POST if self.request.method == "POST" else None, event=self.request.event, request=self.request, initial=initial, instance=self.invoice_address, - validate_vat_id=self.eu_reverse_charge_relevant) + validate_vat_id=self.eu_reverse_charge_relevant, all_optional=self.all_optional) def post(self, request): self.request = request @@ -383,23 +390,25 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): self.request = request try: emailval = EmailValidator() - if 'email' not in self.cart_session: + if not self.cart_session.get('email') and not self.all_optional: if warn: messages.warning(request, _('Please enter a valid email address.')) return False - emailval(self.cart_session.get('email')) + if self.cart_session.get('email'): + emailval(self.cart_session.get('email')) except ValidationError: if warn: messages.warning(request, _('Please enter a valid email address.')) return False - if request.event.settings.invoice_address_required and (not self.invoice_address or not self.invoice_address.street): - messages.warning(request, _('Please enter your invoicing address.')) - return False + if not self.all_optional: + if request.event.settings.invoice_address_required and (not self.invoice_address or not self.invoice_address.street): + messages.warning(request, _('Please enter your invoicing address.')) + return False - if request.event.settings.invoice_name_required and (not self.invoice_address or not self.invoice_address.name): - messages.warning(request, _('Please enter your name.')) - return False + if request.event.settings.invoice_name_required and (not self.invoice_address or not self.invoice_address.name): + messages.warning(request, _('Please enter your name.')) + return False for cp in self._positions_for_questions: answ = { @@ -585,6 +594,8 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): @cached_property def confirm_messages(self): + if self.all_optional: + return {} msgs = {} responses = checkout_confirm_messages.send(self.request.event) for receiver, response in responses: @@ -603,10 +614,17 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): return self.get_result(request) return TemplateFlowStep.get(self, request) + @cached_property + def all_optional(self): + for recv, resp in checkout_all_optional.send(sender=self.request.event, request=self.request): + if resp: + return True + return False + def post(self, request): self.request = request - if self.confirm_messages: + if self.confirm_messages and not self.all_optional: for key, msg in self.confirm_messages.items(): if request.POST.get('confirm_{}'.format(key)) != 'yes': msg = str(_('You need to check all checkboxes on the bottom of the page.')) diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 101c49f46..6d442d654 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -29,12 +29,13 @@ class ContactForm(forms.Form): def __init__(self, *args, **kwargs): self.event = kwargs.pop('event') self.request = kwargs.pop('request') + self.all_optional = kwargs.pop('all_optional', False) super().__init__(*args, **kwargs) if self.event.settings.order_email_asked_twice: self.fields['email_repeat'] = forms.EmailField( label=_('E-mail address (repeated)'), - help_text=_('Please enter the same email address again to make sure you typed it correctly.') + help_text=_('Please enter the same email address again to make sure you typed it correctly.'), ) if not self.request.session.get('iframe_session', False): @@ -44,10 +45,14 @@ class ContactForm(forms.Form): self.fields['email'].widget.attrs['autofocus'] = 'autofocus' responses = contact_form_fields.send(self.event, request=self.request) - for r, response in sorted(responses, key=lambda r: str(r[0])): + for r, response in responses: for key, value in response.items(): # We need to be this explicit, since OrderedDict.update does not retain ordering self.fields[key] = value + if self.all_optional: + for k, v in self.fields.items(): + v.required = False + v.widget.is_required = False def clean(self): if self.event.settings.order_email_asked_twice and self.cleaned_data.get('email') and self.cleaned_data.get('email_repeat'): diff --git a/src/pretix/presale/signals.py b/src/pretix/presale/signals.py index 09f21915d..9b03f61d2 100644 --- a/src/pretix/presale/signals.py +++ b/src/pretix/presale/signals.py @@ -184,3 +184,14 @@ of products. As with all plugin signals, the ``sender`` keyword argument will contain the event. The receivers are expected to return HTML. """ + +checkout_all_optional = EventPluginSignal( + providing_args=['request'] +) +""" +If any receiver of this signal returns ``True``, all input fields during checkout (contact data, +invoice address, confirmations) will be optional, except for questions. Use with care! + +As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``request`` +argument will contain the request object. +"""