diff --git a/doc/api/resources/questions.rst b/doc/api/resources/questions.rst
index 7fed162ef0..b58c3eed05 100644
--- a/doc/api/resources/questions.rst
+++ b/doc/api/resources/questions.rst
@@ -31,6 +31,7 @@ type string The expected ty
* ``H`` – time
* ``W`` – date and time
* ``CC`` – country code (ISO 3666-1 alpha-2)
+ * ``TEL`` – telephone number
required boolean If ``true``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.
diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py
index 09766fc742..80d9a74bff 100644
--- a/src/pretix/api/serializers/item.py
+++ b/src/pretix/api/serializers/item.py
@@ -267,6 +267,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
seen_ids.add(dep.pk)
dep = dep.dependency_question
+ if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
+ raise ValidationError(_('This type of question cannot be asked during check-in.'))
+
Question.clean_items(event, full_data.get('items'))
return data
diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py
index 91f3978967..6b9ef0c8d9 100644
--- a/src/pretix/base/forms/questions.py
+++ b/src/pretix/base/forms/questions.py
@@ -9,6 +9,7 @@ import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
+from babel import localedata
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
@@ -21,11 +22,16 @@ from django.utils.translation import (
)
from django_countries import countries
from django_countries.fields import Country, CountryField
+from phonenumber_field.formfields import PhoneNumberField
+from phonenumber_field.phonenumber import PhoneNumber
+from phonenumber_field.widgets import PhoneNumberPrefixWidget
+from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
TimePickerWidget, UploadedFileWidget,
)
+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 (
@@ -179,6 +185,34 @@ class NamePartsFormField(forms.MultiValueField):
return value
+class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
+ def render(self, name, value, attrs=None, renderer=None):
+ output = super().render(name, value, attrs, renderer)
+ return mark_safe(self.format_output(output))
+
+ def format_output(self, rendered_widgets) -> str:
+ return '
%s
' % ''.join(rendered_widgets)
+
+
+def guess_country(event):
+ # Try to guess the initial country from either the country of the merchant
+ # or the locale. This will hopefully save at least some users some scrolling :)
+ locale = get_language()
+ country = event.settings.invoice_address_from_country
+ if not country:
+ valid_countries = countries.countries
+ if '-' in locale:
+ parts = locale.split('-')
+ if parts[1].upper() in valid_countries:
+ country = Country(parts[1].upper())
+ elif parts[0].upper() in valid_countries:
+ country = Country(parts[0].upper())
+ else:
+ if locale in valid_countries:
+ country = Country(locale.upper())
+ return country
+
+
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -328,6 +362,28 @@ class BaseQuestionsForm(forms.Form):
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
+ elif q.type == Question.TYPE_PHONENUMBER:
+ babel_locale = 'en'
+ # Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
+ if localedata.exists(get_language()):
+ babel_locale = get_language()
+ elif localedata.exists(get_language()[:2]):
+ babel_locale = get_language()[:2]
+ with language(babel_locale):
+ default_country = guess_country(event)
+ default_prefix = None
+ for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
+ if str(default_country) in values:
+ default_prefix = prefix
+ field = PhoneNumberField(
+ label=label, required=required,
+ help_text=help_text,
+ # 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.
+ initial=PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix),
+ widget=WrappedPhoneNumberPrefixWidget()
+ )
field.question = q
if answers:
# Cache the answer object for later use
@@ -433,23 +489,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
- # Try to guess the initial country from either the country of the merchant
- # or the locale. This will hopefully save at least some users some scrolling :)
- locale = get_language()
- country = event.settings.invoice_address_from_country
- if not country:
- valid_countries = countries.countries
- if '-' in locale:
- parts = locale.split('-')
- if parts[1].upper() in valid_countries:
- country = Country(parts[1].upper())
- elif parts[0].upper() in valid_countries:
- country = Country(parts[0].upper())
- else:
- if locale in valid_countries:
- country = Country(locale.upper())
-
- kwargs['initial']['country'] = country
+ kwargs['initial']['country'] = guess_country(self.event)
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py
index 31c43522b3..67a4aaae94 100644
--- a/src/pretix/base/models/items.py
+++ b/src/pretix/base/models/items.py
@@ -977,6 +977,7 @@ class Question(LoggedModel):
TYPE_TIME = "H"
TYPE_DATETIME = "W"
TYPE_COUNTRYCODE = "CC"
+ TYPE_PHONENUMBER = "TEL"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
@@ -989,8 +990,10 @@ class Question(LoggedModel):
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
+ (TYPE_PHONENUMBER, _("Phone number")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
+ ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER]
event = models.ForeignKey(
Event,
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index ddf56b367f..26b682924b 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -30,6 +30,7 @@ from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
+from phonenumber_field.phonenumber import PhoneNumber
from pretix.base.banlist import banned
from pretix.base.decimal import round_decimal
@@ -922,6 +923,8 @@ class QuestionAnswer(models.Model):
return self.answer
elif self.question.type == Question.TYPE_COUNTRYCODE and self.answer:
return Country(self.answer).name or self.answer
+ elif self.question.type == Question.TYPE_PHONENUMBER and self.answer:
+ return PhoneNumber.from_string(self.answer).as_international
else:
return self.answer
diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py
index 21e30a61dd..fb53bf8bc1 100644
--- a/src/pretix/control/forms/item.py
+++ b/src/pretix/control/forms/item.py
@@ -77,6 +77,14 @@ class QuestionForm(I18nModelForm):
dep = dep.dependency_question
return val
+ def clean_ask_during_checkin(self):
+ val = self.cleaned_data.get('ask_during_checkin')
+
+ if val and self.cleaned_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
+ raise ValidationError(_('This type of question cannot be asked during check-in.'))
+
+ return val
+
def clean(self):
d = super().clean()
if d.get('dependency_question') and not d.get('dependency_values'):
diff --git a/src/pretix/helpers/json.py b/src/pretix/helpers/json.py
index 9dc95fc093..a9c3b9db43 100644
--- a/src/pretix/helpers/json.py
+++ b/src/pretix/helpers/json.py
@@ -1,5 +1,6 @@
from django.core.files import File
from i18nfield.utils import I18nJSONEncoder
+from phonenumber_field.phonenumber import PhoneNumber
from pretix.base.reldate import RelativeDateWrapper
@@ -10,6 +11,8 @@ class CustomJSONEncoder(I18nJSONEncoder):
return obj.to_string()
elif isinstance(obj, File):
return obj.name
+ if isinstance(obj, PhoneNumber):
+ return str(obj)
else:
return super().default(obj)
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 6adeaad011..e39f7765f6 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -292,6 +292,7 @@ INSTALLED_APPS = [
'hijack',
'compat',
'oauth2_provider',
+ 'phonenumber_field'
]
try:
diff --git a/src/requirements/production.txt b/src/requirements/production.txt
index ef1eeb0420..d99e7404ae 100644
--- a/src/requirements/production.txt
+++ b/src/requirements/production.txt
@@ -60,3 +60,5 @@ django-localflavor>=2.2
urllib3==1.24.*
django-redis==4.10.*
redis==3.2.*
+django-phonenumber-field==3.0.*
+phonenumberslite==8.10.*
diff --git a/src/setup.py b/src/setup.py
index 9d24967df3..4a23e175c5 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -146,6 +146,8 @@ setup(
'django-oauth-toolkit==1.2.*',
'oauthlib==2.1.*',
'urllib3==1.24.*', # required by current requests
+ 'django-phonenumber-field==3.0.*',
+ 'phonenumberslite==8.10.*',
],
extras_require={
'dev': [