Add question type: phone number (#1462)

* Add Phonenumber-Field as to Questions

* Add setup requirements

* Add list of ask-during-checkin restricted question types and enforce it

* Fix requirements

* Fix crash using custom locales

* Re-format phone numbers when outputting to humans

* Initialize country code field with a guess for the customer's country

* Document TEL type in API docs
This commit is contained in:
Martin Gross
2019-11-13 12:52:07 +01:00
committed by Raphael Michel
parent 06a744ea2d
commit 54091b9721
10 changed files with 83 additions and 17 deletions

View File

@@ -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

View File

@@ -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 '<div class="nameparts-form-group">%s</div>' % ''.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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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'):

View File

@@ -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)

View File

@@ -292,6 +292,7 @@ INSTALLED_APPS = [
'hijack',
'compat',
'oauth2_provider',
'phonenumber_field'
]
try:

View File

@@ -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.*

View File

@@ -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': [