diff --git a/doc/api/resources/waitinglist.rst b/doc/api/resources/waitinglist.rst index b4924981a0..581e775f73 100644 --- a/doc/api/resources/waitinglist.rst +++ b/doc/api/resources/waitinglist.rst @@ -13,7 +13,10 @@ Field Type Description ===================================== ========================== ======================================================= id integer Internal ID of the waiting list entry created datetime Creation date of the waiting list entry +name string Name of the user on the waiting list (or ``null``) +name_parts object of strings Decomposition of name of the user (or ``null``) email string Email address of the user on the waiting list +phone string Phone number of the user on the waiting list (or ``null``) voucher integer Internal ID of the voucher sent to this user. If this field is set, the user has been sent a voucher and is no longer waiting. If it is ``null``, the diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 3b512bd99b..96d43bb928 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -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', diff --git a/src/pretix/api/serializers/waitinglist.py b/src/pretix/api/serializers/waitinglist.py index 8be870d985..81b38f50c5 100644 --- a/src/pretix/api/serializers/waitinglist.py +++ b/src/pretix/api/serializers/waitinglist.py @@ -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 diff --git a/src/pretix/base/exporters/waitinglist.py b/src/pretix/base/exporters/waitinglist.py index c6a00d4542..80697dc686 100644 --- a/src/pretix/base/exporters/waitinglist.py +++ b/src/pretix/base/exporters/waitinglist.py @@ -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, diff --git a/src/pretix/base/migrations/0177_auto_20210301_1510.py b/src/pretix/base/migrations/0177_auto_20210301_1510.py new file mode 100644 index 0000000000..7a2795ed2e --- /dev/null +++ b/src/pretix/base/migrations/0177_auto_20210301_1510.py @@ -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), + ), + ] diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 57def028ed..2dd5bcc0fa 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -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) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 1c0658e229..f464dc7265 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -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, diff --git a/src/pretix/base/shredder.py b/src/pretix/base/shredder.py index 0fbe4561e5..d7fd89093d 100644 --- a/src/pretix/base/shredder.py +++ b/src/pretix/base/shredder.py @@ -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']) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 05d7d66419..0bfb9c77f0 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -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', diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index c6423e1447..eac6830431 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -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" %}