forked from CGM_Public/pretix_original
Add attendee email field (#466)
* Add attendee email field * exports, tests
This commit is contained in:
@@ -68,6 +68,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'variation': position.variation_id,
|
'variation': position.variation_id,
|
||||||
'price': position.price,
|
'price': position.price,
|
||||||
'attendee_name': position.attendee_name,
|
'attendee_name': position.attendee_name,
|
||||||
|
'attendee_email': position.attendee_email,
|
||||||
'secret': position.secret,
|
'secret': position.secret,
|
||||||
'answers': [
|
'answers': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django import forms
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.models import OrderPosition
|
||||||
from ..exporter import BaseExporter
|
from ..exporter import BaseExporter
|
||||||
from ..models import Order
|
from ..models import Order
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
@@ -16,7 +17,11 @@ class MailExporter(BaseExporter):
|
|||||||
def render(self, form_data: dict):
|
def render(self, form_data: dict):
|
||||||
qs = self.event.orders.filter(status__in=form_data['status'])
|
qs = self.event.orders.filter(status__in=form_data['status'])
|
||||||
addrs = qs.values('email')
|
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")
|
return 'pretixemails.txt', 'text/plain', data.encode("utf-8")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
40
src/pretix/base/migrations/0055_auto_20170413_1537.py
Normal file
40
src/pretix/base/migrations/0055_auto_20170413_1537.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -394,6 +394,8 @@ class AbstractPosition(models.Model):
|
|||||||
:type price: decimal.Decimal
|
:type price: decimal.Decimal
|
||||||
:param attendee_name: The attendee's name, if entered.
|
:param attendee_name: The attendee's name, if entered.
|
||||||
:type attendee_name: str
|
: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
|
:param voucher: A voucher that has been applied to this sale
|
||||||
:type voucher: Voucher
|
:type voucher: Voucher
|
||||||
"""
|
"""
|
||||||
@@ -418,6 +420,11 @@ class AbstractPosition(models.Model):
|
|||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text=_("Empty, if this product is not an admission ticket")
|
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 = models.ForeignKey(
|
||||||
'Voucher', null=True, blank=True
|
'Voucher', null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ DEFAULTS = {
|
|||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool
|
'type': bool
|
||||||
},
|
},
|
||||||
|
'attendee_emails_asked': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
|
'attendee_emails_required': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
'invoice_address_asked': {
|
'invoice_address_asked': {
|
||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
|
|||||||
@@ -240,6 +240,21 @@ class EventSettingsForm(SettingsForm):
|
|||||||
help_text=_("Require customers to fill in the names of all attendees."),
|
help_text=_("Require customers to fill in the names of all attendees."),
|
||||||
required=False
|
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(
|
max_items_per_order = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=1,
|
||||||
label=_("Maximum number of items per order")
|
label=_("Maximum number of items per order")
|
||||||
@@ -274,6 +289,11 @@ class EventSettingsForm(SettingsForm):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
|
'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
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
{% bootstrap_field sform.max_items_per_order layout="horizontal" %}
|
{% bootstrap_field sform.max_items_per_order layout="horizontal" %}
|
||||||
{% bootstrap_field sform.attendee_names_asked layout="horizontal" %}
|
{% bootstrap_field sform.attendee_names_asked layout="horizontal" %}
|
||||||
{% bootstrap_field sform.attendee_names_required 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" %}
|
{% bootstrap_field sform.cancel_allow_user layout="horizontal" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -187,6 +187,11 @@
|
|||||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
|
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
|
||||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||||
{% endif %}
|
{% 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 %}
|
{% for q in line.questions %}
|
||||||
<dt>{{ q.question }}</dt>
|
<dt>{{ q.question }}</dt>
|
||||||
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}
|
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class OrderList(EventPermissionRequiredMixin, ListView):
|
|||||||
u = self.request.GET.get("user", "")
|
u = self.request.GET.get("user", "")
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(email__icontains=u) | Q(positions__attendee_name__icontains=u)
|
Q(email__icontains=u) | Q(positions__attendee_name__icontains=u)
|
||||||
|
| Q(positions__attendee_email__icontains=u)
|
||||||
)
|
)
|
||||||
if self.request.GET.get("status", "") != "":
|
if self.request.GET.get("status", "") != "":
|
||||||
s = self.request.GET.get("status", "")
|
s = self.request.GET.get("status", "")
|
||||||
@@ -172,6 +173,7 @@ class OrderDetail(OrderView):
|
|||||||
for p in cartpos:
|
for p in cartpos:
|
||||||
p.has_questions = (
|
p.has_questions = (
|
||||||
(p.item.admission and self.request.event.settings.attendee_names_asked) or
|
(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.item.questions.all()
|
||||||
)
|
)
|
||||||
p.cache_answers()
|
p.cache_answers()
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ class CSVCheckinList(BaseCheckinList):
|
|||||||
if form_data['secrets']:
|
if form_data['secrets']:
|
||||||
headers.append(_('Secret'))
|
headers.append(_('Secret'))
|
||||||
|
|
||||||
|
if self.event.settings.attendee_emails_asked:
|
||||||
|
headers.append(_('E-mail'))
|
||||||
|
|
||||||
for q in questions:
|
for q in questions:
|
||||||
headers.append(str(q.question))
|
headers.append(str(q.question))
|
||||||
|
|
||||||
@@ -105,6 +108,8 @@ class CSVCheckinList(BaseCheckinList):
|
|||||||
row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No'))
|
row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No'))
|
||||||
if form_data['secrets']:
|
if form_data['secrets']:
|
||||||
row.append(op.secret)
|
row.append(op.secret)
|
||||||
|
if self.event.settings.attendee_emails_asked:
|
||||||
|
row.append(op.attendee_email)
|
||||||
acache = {}
|
acache = {}
|
||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
acache[a.question_id] = str(a)
|
acache[a.question_id] = str(a)
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|||||||
if warn:
|
if warn:
|
||||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ from pretix.base.models.orders import InvoiceAddress
|
|||||||
|
|
||||||
|
|
||||||
class ContactForm(forms.Form):
|
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):
|
class InvoiceAddressForm(forms.ModelForm):
|
||||||
@@ -67,6 +70,12 @@ class QuestionsForm(forms.Form):
|
|||||||
label=_('Attendee name'),
|
label=_('Attendee name'),
|
||||||
initial=(cartpos.attendee_name if cartpos else orderpos.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:
|
for q in questions:
|
||||||
# Do we already have an answer? Provide it as the initial value
|
# Do we already have an answer? Provide it as the initial value
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<dt>{% trans "Attendee name" %}</dt>
|
<dt>{% trans "Attendee name" %}</dt>
|
||||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||||
{% endif %}
|
{% 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 %}
|
{% for q in line.questions %}
|
||||||
<dt>{{ q.question }}</dt>
|
<dt>{{ q.question }}</dt>
|
||||||
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||||
|
|||||||
@@ -46,8 +46,13 @@ class CartMixin:
|
|||||||
i = pos.pk
|
i = pos.pk
|
||||||
if downloads:
|
if downloads:
|
||||||
return i, pos.pk, 0, 0, 0, 0,
|
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 i, pos.pk, 0, 0, 0, 0,
|
||||||
return 0, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0)
|
return 0, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class QuestionsViewMixin:
|
|||||||
if k == 'attendee_name':
|
if k == 'attendee_name':
|
||||||
form.pos.attendee_name = v if v != '' else None
|
form.pos.attendee_name = v if v != '' else None
|
||||||
form.pos.save()
|
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:
|
elif k.startswith('question_') and v is not None:
|
||||||
field = form.fields[k]
|
field = form.fields[k]
|
||||||
if hasattr(field, 'answer'):
|
if hasattr(field, 'answer'):
|
||||||
|
|||||||
@@ -96,6 +96,36 @@ class CheckoutTestCase(TestCase):
|
|||||||
self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
|
self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
|
||||||
self.assertFalse(cr2.answers.filter(question=q2).exists())
|
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):
|
def test_attendee_name_required(self):
|
||||||
self.event.settings.set('attendee_names_asked', True)
|
self.event.settings.set('attendee_names_asked', True)
|
||||||
self.event.settings.set('attendee_names_required', True)
|
self.event.settings.set('attendee_names_required', True)
|
||||||
|
|||||||
Reference in New Issue
Block a user