mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
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:
committed by
GitHub
parent
8ca2fe7707
commit
8e00970f04
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
30
src/pretix/base/migrations/0177_auto_20210301_1510.py
Normal file
30
src/pretix/base/migrations/0177_auto_20210301_1510.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user