Add attendee email field (#466)

* Add attendee email field

* exports, tests
This commit is contained in:
Raphael Michel
2017-04-13 22:59:54 +02:00
committed by GitHub
parent 3c59a870e7
commit e4706dd3ba
16 changed files with 155 additions and 4 deletions

View File

@@ -68,6 +68,7 @@ class JSONExporter(BaseExporter):
'variation': position.variation_id,
'price': position.price,
'attendee_name': position.attendee_name,
'attendee_email': position.attendee_email,
'secret': position.secret,
'answers': [
{

View File

@@ -4,6 +4,7 @@ from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import OrderPosition
from ..exporter import BaseExporter
from ..models import Order
from ..signals import register_data_exporters
@@ -16,7 +17,11 @@ class MailExporter(BaseExporter):
def render(self, form_data: dict):
qs = self.event.orders.filter(status__in=form_data['status'])
addrs = qs.values('email')
data = "\r\n".join(set(a['email'] for a in addrs))
pos = OrderPosition.objects.filter(
order__event=self.event, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs)
| set(a['attendee_email'] for a in pos if a['attendee_email']))
return 'pretixemails.txt', 'text/plain', data.encode("utf-8")
@property

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-13 15:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0054_auto_20170413_1050'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='attendee_email',
field=models.EmailField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=254, null=True, verbose_name='Attendee email'),
),
migrations.AddField(
model_name='orderposition',
name='attendee_email',
field=models.EmailField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=254, null=True, verbose_name='Attendee email'),
),
migrations.AlterField(
model_name='event_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='globalsettingsobject_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='organizer_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
]

View File

@@ -394,6 +394,8 @@ class AbstractPosition(models.Model):
:type price: decimal.Decimal
:param attendee_name: The attendee's name, if entered.
:type attendee_name: str
:param attendee_email: The attendee's email, if entered.
:type attendee_email: str
:param voucher: A voucher that has been applied to this sale
:type voucher: Voucher
"""
@@ -418,6 +420,11 @@ class AbstractPosition(models.Model):
blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket")
)
attendee_email = models.EmailField(
verbose_name=_("Attendee email"),
blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket")
)
voucher = models.ForeignKey(
'Voucher', null=True, blank=True
)

View File

@@ -27,6 +27,14 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'attendee_emails_asked': {
'default': 'False',
'type': bool
},
'attendee_emails_required': {
'default': 'False',
'type': bool
},
'invoice_address_asked': {
'default': 'True',
'type': bool,

View File

@@ -240,6 +240,21 @@ class EventSettingsForm(SettingsForm):
help_text=_("Require customers to fill in the names of all attendees."),
required=False
)
attendee_emails_asked = forms.BooleanField(
label=_("Ask for attendee e-mails"),
help_text=_("Ask for an e-mail address for all tickets which include admission to the event. Important: For "
"every order, an e-mail address needs to be provided, regardless of this setting. The order "
"confirmation which can be used to change the order will always only be sent to the e-mail "
"address provided for the order. Only check this box if you want to ask for additional e-mail "
"addresses for each attendee, e.g. in case of group orders."),
required=False
)
attendee_emails_required = forms.BooleanField(
label=_("Require attendee e-mails"),
help_text=_("Require customers to fill in the e-mail addresses of all attendees. See the above option for "
"more details."),
required=False
)
max_items_per_order = forms.IntegerField(
min_value=1,
label=_("Maximum number of items per order")
@@ -274,6 +289,11 @@ class EventSettingsForm(SettingsForm):
raise ValidationError({
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
})
if data['attendee_emails_required'] and not data['attendee_emails_asked']:
raise ValidationError({
'attendee_emails_required': _('You cannot require specifying attendee emails if you do not ask for '
'them.')
})
return data

View File

@@ -41,6 +41,8 @@
{% bootstrap_field sform.max_items_per_order layout="horizontal" %}
{% bootstrap_field sform.attendee_names_asked layout="horizontal" %}
{% bootstrap_field sform.attendee_names_required layout="horizontal" %}
{% bootstrap_field sform.attendee_emails_asked layout="horizontal" %}
{% bootstrap_field sform.attendee_emails_required layout="horizontal" %}
{% bootstrap_field sform.cancel_allow_user layout="horizontal" %}
</fieldset>
<fieldset>

View File

@@ -187,6 +187,11 @@
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt>
<dd>{% if line.attendee_email %}{{ line.attendee_email }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% for q in line.questions %}
<dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}

View File

@@ -54,6 +54,7 @@ class OrderList(EventPermissionRequiredMixin, ListView):
u = self.request.GET.get("user", "")
qs = qs.filter(
Q(email__icontains=u) | Q(positions__attendee_name__icontains=u)
| Q(positions__attendee_email__icontains=u)
)
if self.request.GET.get("status", "") != "":
s = self.request.GET.get("status", "")
@@ -172,6 +173,7 @@ class OrderDetail(OrderView):
for p in cartpos:
p.has_questions = (
(p.item.admission and self.request.event.settings.attendee_names_asked) or
(p.item.admission and self.request.event.settings.attendee_emails_asked) or
p.item.questions.all()
)
p.cache_answers()

View File

@@ -89,6 +89,9 @@ class CSVCheckinList(BaseCheckinList):
if form_data['secrets']:
headers.append(_('Secret'))
if self.event.settings.attendee_emails_asked:
headers.append(_('E-mail'))
for q in questions:
headers.append(str(q.question))
@@ -105,6 +108,8 @@ class CSVCheckinList(BaseCheckinList):
row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No'))
if form_data['secrets']:
row.append(op.secret)
if self.event.settings.attendee_emails_asked:
row.append(op.attendee_email)
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)

View File

@@ -202,6 +202,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
and cp.attendee_email is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
return True
def get_context_data(self, **kwargs):

View File

@@ -9,7 +9,10 @@ from pretix.base.models.orders import InvoiceAddress
class ContactForm(forms.Form):
email = forms.EmailField(label=_('E-mail'))
email = forms.EmailField(label=_('E-mail'),
help_text=_('Make sure to enter a valid email address. We will send you an order '
'confirmation including a link that you need in case you want to make '
'modifications to your order or download your ticket later.'))
class InvoiceAddressForm(forms.ModelForm):
@@ -67,6 +70,12 @@ class QuestionsForm(forms.Form):
label=_('Attendee name'),
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name)
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
)
for q in questions:
# Do we already have an answer? Provide it as the initial value

View File

@@ -18,6 +18,10 @@
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_emails_asked%}
<dt>{% trans "Attendee email" %}</dt>
<dd>{% if line.attendee_email %}{{ line.attendee_email }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% for q in line.questions %}
<dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>

View File

@@ -46,8 +46,13 @@ class CartMixin:
i = pos.pk
if downloads:
return i, pos.pk, 0, 0, 0, 0,
if answers and ((pos.item.admission and self.request.event.settings.attendee_names_asked)
or pos.item.questions.all()):
has_attendee_data = pos.item.admission and (
self.request.event.settings.attendee_names_asked
or self.request.event.settings.attendee_emails_asked
)
if answers and (has_attendee_data or pos.item.questions.all()):
return i, pos.pk, 0, 0, 0, 0,
return 0, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0)

View File

@@ -45,6 +45,9 @@ class QuestionsViewMixin:
if k == 'attendee_name':
form.pos.attendee_name = v if v != '' else None
form.pos.save()
elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None
form.pos.save()
elif k.startswith('question_') and v is not None:
field = form.fields[k]
if hasattr(field, 'answer'):

View File

@@ -96,6 +96,36 @@ class CheckoutTestCase(TestCase):
self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
self.assertFalse(cr2.answers.filter(question=q2).exists())
def test_attendee_email_required(self):
self.event.settings.set('attendee_emails_asked', True)
self.event.settings.set('attendee_emails_required', True)
cr1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select('input[name=%s-attendee_email]' % cr1.id)), 1)
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_email' % cr1.id: '',
'email': 'admin@localhost'
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_email' % cr1.id: 'foo@localhost',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
cr1 = CartPosition.objects.get(id=cr1.id)
self.assertEqual(cr1.attendee_email, 'foo@localhost')
def test_attendee_name_required(self):
self.event.settings.set('attendee_names_asked', True)
self.event.settings.set('attendee_names_required', True)