mirror of
https://github.com/pretix/pretix.git
synced 2026-08-18 12:06:26 +00:00
* New CheckoutSession model, created and deleted throught cart lifetime but only used for order-level question answers so far * Order-level QuestionAnswers (relations to CheckoutSession / Order) * New container_type field on Question model to specify whether Question belongs to order or orderposition * Order-level questions are currently experimental, UI is hidden behind feature flag --------- Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
227 lines
10 KiB
Python
227 lines
10 KiB
Python
#
|
||
# This file is part of pretix (Community Edition).
|
||
#
|
||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||
#
|
||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||
# this file, see <https://pretix.eu/about/en/license>.
|
||
#
|
||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||
# details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||
# <https://www.gnu.org/licenses/>.
|
||
#
|
||
|
||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||
#
|
||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||
#
|
||
# This file contains Apache-licensed contributions copyrighted by: Jan-Frederik Rieckers, Sohalt, Tobias Kunze
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
# License for the specific language governing permissions and limitations under the License.
|
||
|
||
from django import forms
|
||
from django.core.exceptions import ValidationError
|
||
from django.utils.formats import date_format
|
||
from django.utils.html import escape
|
||
from django.utils.safestring import mark_safe
|
||
from django.utils.translation import gettext_lazy as _
|
||
from phonenumber_field.formfields import PhoneNumberField
|
||
|
||
from pretix.base.forms.questions import (
|
||
BaseInvoiceAddressForm, TicketLevelQuestionsForm,
|
||
WrappedPhoneNumberPrefixWidget, guess_phone_prefix_from_request,
|
||
)
|
||
from pretix.base.templatetags.rich_text import rich_text
|
||
from pretix.base.validators import EmailBanlistValidator
|
||
from pretix.presale.signals import contact_form_fields
|
||
|
||
|
||
class ContactForm(forms.Form):
|
||
required_css_class = 'required'
|
||
email = forms.EmailField(label=_('Email'),
|
||
validators=[EmailBanlistValidator()],
|
||
widget=forms.EmailInput(attrs={'autocomplete': 'section-contact email'})
|
||
)
|
||
|
||
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=_('Email address (repeated)'),
|
||
help_text=_('Please enter the same email address again to make sure you typed it correctly.'),
|
||
)
|
||
|
||
if self.event.settings.order_phone_asked:
|
||
if not self.initial.get('phone'):
|
||
phone_prefix = guess_phone_prefix_from_request(self.request, self.event)
|
||
if phone_prefix:
|
||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||
# the future.
|
||
self.initial['phone'] = "+{}.".format(phone_prefix)
|
||
|
||
self.fields['phone'] = PhoneNumberField(
|
||
label=_('Phone number'),
|
||
required=self.event.settings.order_phone_required,
|
||
help_text=rich_text(self.event.settings.checkout_phone_helptext),
|
||
widget=WrappedPhoneNumberPrefixWidget()
|
||
)
|
||
|
||
if not self.request.session.get('iframe_session', False):
|
||
# There is a browser quirk in Chrome that leads to incorrect initial scrolling in iframes if there
|
||
# is an autofocus field. Who would have thought… See e.g. here:
|
||
# https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440
|
||
self.fields['email'].widget.attrs['autofocus'] = 'autofocus'
|
||
self.fields['email'].help_text = rich_text(self.event.settings.checkout_email_helptext)
|
||
|
||
responses = contact_form_fields.send(self.event, request=self.request)
|
||
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'):
|
||
if self.cleaned_data.get('email').lower() != self.cleaned_data.get('email_repeat').lower():
|
||
raise ValidationError(_('Please enter the same email address twice.'))
|
||
|
||
|
||
class InvoiceAddressForm(BaseInvoiceAddressForm):
|
||
required_css_class = 'required'
|
||
vat_warning = True
|
||
address_validation = True
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
allow_save = kwargs.pop('allow_save', False)
|
||
super().__init__(*args, **kwargs)
|
||
if allow_save:
|
||
self.fields['saved_id'] = forms.IntegerField(
|
||
required=False,
|
||
help_text=" ", # non-breaking-space, will be overwritten by JavaScript, needed here for HTML-output
|
||
label=_("Save to address"),
|
||
widget=forms.Select(choices=(("", _("Create new address")),))
|
||
)
|
||
self.fields['save'] = forms.BooleanField(
|
||
label=_('Save address in my customer account for future purchases'),
|
||
required=False,
|
||
initial=False,
|
||
)
|
||
|
||
|
||
class InvoiceNameForm(InvoiceAddressForm):
|
||
address_validation = False
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
for f in list(self.fields.keys()):
|
||
if f != 'name_parts':
|
||
del self.fields[f]
|
||
|
||
|
||
class CustomerAwareQuestionsForm(TicketLevelQuestionsForm):
|
||
"""
|
||
The base class is responsible for asking order-related questions. This includes
|
||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||
as well as additional questions defined by the organizer.
|
||
|
||
This class adds support for pre-filling data like name and address from a
|
||
customer profile, in case the user is logged-in with a customer account.
|
||
"""
|
||
required_css_class = 'required'
|
||
address_validation = True
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
allow_save = kwargs.pop('allow_save', False)
|
||
super().__init__(*args, **kwargs)
|
||
if allow_save and self.fields:
|
||
self.fields['save'] = forms.BooleanField(
|
||
label=_('Save answers to my customer profiles for future purchases'),
|
||
required=False,
|
||
initial=False,
|
||
)
|
||
self.fields['saved_id'] = forms.IntegerField(
|
||
required=False,
|
||
help_text=" ", # non-breaking-space, will be overwritten by JavaScript, needed here for HTML-output
|
||
label=_("Save to profile"),
|
||
widget=forms.Select(choices=(("", _("Create new profile")),))
|
||
)
|
||
|
||
|
||
class MembershipForm(forms.Form):
|
||
required_css_class = 'required'
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.memberships = kwargs.pop('memberships')
|
||
event = kwargs.pop('event')
|
||
self.position = kwargs.pop('position')
|
||
|
||
super().__init__(*args, **kwargs)
|
||
|
||
ev = self.position.subevent or event
|
||
if self.position.variation and self.position.variation.require_membership:
|
||
types = self.position.variation.require_membership_types.all()
|
||
else:
|
||
types = self.position.item.require_membership_types.all()
|
||
|
||
memberships = [
|
||
m for m in self.memberships
|
||
if m.membership_type in types and (
|
||
m.is_valid(ev, self.position.valid_from, valid_from_not_chosen=self.position.item.validity_dynamic_start_choice)
|
||
)
|
||
]
|
||
|
||
if len(memberships) == 1:
|
||
self.initial['membership'] = str(memberships[0].pk)
|
||
|
||
self.fields['membership'] = forms.ChoiceField(
|
||
label=_('Membership'),
|
||
choices=[
|
||
(str(m.pk), self._label_from_instance(m))
|
||
for m in memberships
|
||
],
|
||
widget=forms.RadioSelect,
|
||
)
|
||
self.is_empty = not memberships
|
||
|
||
def _label_from_instance(self, obj):
|
||
ds = date_format(obj.date_start, 'SHORT_DATE_FORMAT')
|
||
de = date_format(obj.date_end, 'SHORT_DATE_FORMAT')
|
||
if obj.membership_type.max_usages is not None:
|
||
usages = f'({obj.usages} / {obj.membership_type.max_usages})'
|
||
else:
|
||
usages = ''
|
||
d = f'<strong>{escape(obj.membership_type)}</strong> {usages}<br>'
|
||
if obj.attendee_name:
|
||
d += f'{escape(obj.attendee_name)}<br>'
|
||
d += f'<span class="text-muted">{ds} – {de}</span>'
|
||
if obj.testmode:
|
||
d += ' <span class="label label-warning">{}</span>'.format(_("TEST MODE"))
|
||
return mark_safe(d)
|
||
|
||
def clean(self):
|
||
d = super().clean()
|
||
if d.get('membership'):
|
||
d['membership'] = [m for m in self.memberships if str(m.pk) == d['membership']][0]
|
||
return d
|