Waiting list: Add name and phone number (#1987)

* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 19.9% (799 of 3996 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 21.6% (865 of 3996 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 23.8% (955 of 3996 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 26.3% (1051 of 3996 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Revert more of de597ba86

* Paginate list of gift cards

* Change texts on order confirmation page if no attachments are sent

* Update locales

* Added translation on translate.pretix.eu (Sinhala)

* Added translation on translate.pretix.eu (Sinhala)

* Translated on translate.pretix.eu (Sinhala)

Currently translated at 0.4% (18 of 4002 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/si/

powered by weblate

* Fix initial value of phone number

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* update API-description with null-fields

* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Fix initial value of phone number

* update API-description with null-fields

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* fixed isort on migration

Co-authored-by: lapor-kris <kristijan.tkalec@posteo.si>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: helabasa <R45XvezA@pm.me>
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Richard Schreiber
2021-03-05 10:02:37 +01:00
committed by GitHub
parent 8ca2fe7707
commit 8e00970f04
16 changed files with 227 additions and 9 deletions

View File

@@ -614,6 +614,11 @@ class EventSettingsSerializer(SettingsSerializer):
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order',
'reservation_time',
'contact_mail',

View File

@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
read_only_fields = ('id', 'created', 'voucher')
def validate(self, data):
@@ -32,4 +32,11 @@ class WaitingListSerializer(I18nAwareModelSerializer):
if availability[0] == 100:
raise ValidationError("This product is currently available.")
if data.get('name') and data.get('name_parts'):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = event.settings.name_scheme
return data

View File

@@ -82,7 +82,9 @@ class WaitingListExporter(ListExporter):
headers = [
_('Date'),
_('Name'),
_('Email'),
_('Phone number'),
_('Product name'),
_('Variation'),
_('Event slug'),
@@ -117,7 +119,9 @@ class WaitingListExporter(ListExporter):
row = [
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
entry.name,
entry.email,
entry.phone,
str(entry.item) if entry.item else "",
str(entry.variation) if entry.variation else "",
entry.event.slug,

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.0.10 on 2021-03-01 15:10
import jsonfallback.fields
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0176_auto_20210205_1512'),
]
operations = [
migrations.AddField(
model_name='waitinglistentry',
name='name_cached',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='waitinglistentry',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AddField(
model_name='waitinglistentry',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
),
]

View File

@@ -5,11 +5,14 @@ from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from jsonfallback.fields import FallbackJSONField
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LoggedModel
from .event import Event, SubEvent
@@ -37,9 +40,21 @@ class WaitingListEntry(LoggedModel):
verbose_name=_("On waiting list since"),
auto_now_add=True
)
name_cached = models.CharField(
max_length=255,
verbose_name=_("Name"),
blank=True, null=True,
)
name_parts = FallbackJSONField(
blank=True, default=dict
)
email = models.EmailField(
verbose_name=_("E-mail address")
)
phone = PhoneNumberField(
null=True, blank=True,
verbose_name=_("Phone number")
)
voucher = models.ForeignKey(
'Voucher',
verbose_name=_("Assigned voucher"),
@@ -83,6 +98,27 @@ class WaitingListEntry(LoggedModel):
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
if 'name_parts' in update_fields:
update_fields.append('name_cached')
self.name_cached = self.name
if self.name_parts is None:
self.name_parts = {}
super().save(*args, **kwargs)
@property
def name(self):
if not self.name_parts:
return None
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.name_parts).strip()
def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)

View File

@@ -975,6 +975,61 @@ DEFAULTS = {
widget=forms.NumberInput(),
)
},
'waiting_list_names_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a name"),
help_text=_("Ask for a name when signing up to the waiting list."),
)
},
'waiting_list_names_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require name"),
help_text=_("Require a name when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_names_asked'}),
)
},
'waiting_list_phones_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a phone number"),
help_text=_("Ask for a phone number when signing up to the waiting list."),
)
},
'waiting_list_phones_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require phone number"),
help_text=_("Require a phone number when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_phones_asked'}),
)
},
'waiting_list_phones_explanation_text': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Phone number explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
},
'ticket_download': {
'default': 'False',
'type': bool,

View File

@@ -203,7 +203,7 @@ class EmailAddressShredder(BaseDataShredder):
class WaitingListShredder(BaseDataShredder):
verbose_name = _('Waiting list')
identifier = 'waiting_list'
description = _('This will remove all email addresses from the waiting list.')
description = _('This will remove all names, email addresses, and phone numbers from the waiting list.')
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'waiting-list.json', 'application/json', json.dumps([
@@ -213,7 +213,7 @@ class WaitingListShredder(BaseDataShredder):
@transaction.atomic
def shred_data(self):
self.event.waitinglistentries.update(email='')
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='', phone='')
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
if '@' in wle.voucher.comment:
@@ -222,7 +222,14 @@ class WaitingListShredder(BaseDataShredder):
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
d = le.parsed_data
if 'name' in d:
d['name'] = ''
if 'name_parts' in d:
d['name_parts'] = {
'_legacy': ''
}
d['email'] = ''
d['phone'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])

View File

@@ -449,6 +449,11 @@ class EventSettingsForm(SettingsForm):
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order',
'reservation_time',
'contact_mail',

View File

@@ -248,6 +248,9 @@
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours layout="control" %}
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Item metadata" %}</legend>

View File

@@ -132,7 +132,13 @@
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "User" %}</th>
{% if request.event.settings.waiting_list_names_asked %}
<th>{% trans "Name" %}</th>
{% endif %}
<th>{% trans "Email" %}</th>
{% if request.event.settings.waiting_list_phones_asked %}
<th>{% trans "Phone number" %}</th>
{% endif %}
<th>{% trans "Product" %}</th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
@@ -146,7 +152,13 @@
<tbody>
{% for e in entries %}
<tr>
{% if request.event.settings.waiting_list_names_asked %}
<td>{{ e.name|default:"" }}</td>
{% endif %}
<td>{{ e.email }}</td>
{% if request.event.settings.waiting_list_phones_asked %}
<td>{{ e.phone|default:"" }}</td>
{% endif %}
<td>
{{ e.item }}
{% if e.variation %}

View File

@@ -211,7 +211,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
headers = [
_('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
_('Name'), _('E-mail address'), _('Phone number'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
_('Language'), _('Priority')
]
if self.request.event.has_subevents:
@@ -235,7 +235,9 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
status = _('Waiting')
row = [
w.name,
w.email,
w.phone,
prod,
w.created.isoformat(),
status,

View File

@@ -1,13 +1,54 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, guess_country,
)
from pretix.base.i18n import get_babel_locale, language
from pretix.base.models import WaitingListEntry
class WaitingListForm(forms.ModelForm):
required_css_class = 'required'
class Meta:
model = WaitingListEntry
fields = ('email',)
fields = ('name_parts', 'email', 'phone')
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
event = self.event
if event.settings.waiting_list_names_asked:
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.waiting_list_names_required,
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Name'),
)
else:
del self.fields['name_parts']
if event.settings.waiting_list_phones_asked:
with language(get_babel_locale()):
default_country = guess_country(self.event)
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if str(default_country) in values and not self.initial.get('phone'):
# 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(prefix)
self.fields['phone'] = PhoneNumberField(
label=_("Phone number"),
required=event.settings.waiting_list_phones_required,
help_text=event.settings.waiting_list_phones_explanation_text,
widget=WrappedPhoneNumberPrefixWidget()
)
else:
del self.fields['phone']

View File

@@ -22,7 +22,7 @@
</div>
</div>
{% endif %}
{% bootstrap_form form layout='horizontal' %}
{% bootstrap_form form layout="checkout" %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="help-block">