mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
* Allow to directly bulk-send vouchers via email * Send mails * Log messages * Fix test failures * Add new test cases
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import csv
|
||||
from collections import namedtuple
|
||||
from io import StringIO
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models.functions import Lower
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.models import Item, Voucher
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||
@@ -191,6 +197,54 @@ class VoucherBulkForm(VoucherForm):
|
||||
),
|
||||
required=True
|
||||
)
|
||||
send = forms.BooleanField(
|
||||
label=_("Send vouchers via email"),
|
||||
required=False
|
||||
)
|
||||
send_subject = forms.CharField(
|
||||
label=_("Subject"),
|
||||
widget=forms.TextInput(attrs={'data-display-dependency': '#id_send'}),
|
||||
required=False,
|
||||
initial=_('Your voucher for {event}')
|
||||
)
|
||||
send_message = forms.CharField(
|
||||
label=_("Message"),
|
||||
widget=forms.Textarea(attrs={'data-display-dependency': '#id_send'}),
|
||||
required=False,
|
||||
initial=_('Hello,\n\n'
|
||||
'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n'
|
||||
'You can redeem them here in our ticket shop:\n\n{url}\n\nBest regards,\n\n'
|
||||
'Your {event} team')
|
||||
)
|
||||
send_recipients = forms.CharField(
|
||||
label=_('Recipients'),
|
||||
widget=forms.Textarea(attrs={
|
||||
'data-display-dependency': '#id_send',
|
||||
'placeholder': 'email,number,name,tag\njohn@example.org,3,John,example\n\n-- {} --\n\njohn@example.org\njane@example.net'.format(
|
||||
_('or')
|
||||
)
|
||||
}),
|
||||
required=False,
|
||||
help_text=_('You can either supply a list of email addresses with one email address per line, or a CSV file with a title column '
|
||||
'and one or more of the columns "email", "number", "name", or "tag".')
|
||||
)
|
||||
Recipient = namedtuple('Recipient', 'email number name tag')
|
||||
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
phs = [
|
||||
'{%s}' % p
|
||||
for p in sorted(get_available_placeholders(self.instance.event, base_parameters).keys())
|
||||
]
|
||||
ht = _('Available placeholders: {list}').format(
|
||||
list=', '.join(phs)
|
||||
)
|
||||
if self.fields[fn].help_text:
|
||||
self.fields[fn].help_text += ' ' + str(ht)
|
||||
else:
|
||||
self.fields[fn].help_text = ht
|
||||
self.fields[fn].validators.append(
|
||||
PlaceholderValidator(phs)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
@@ -213,6 +267,51 @@ class VoucherBulkForm(VoucherForm):
|
||||
'max_usages': _('Number of times times EACH of these vouchers can be redeemed.')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._set_field_placeholders('send_subject', ['event', 'name'])
|
||||
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
|
||||
|
||||
def clean_send_recipients(self):
|
||||
raw = self.cleaned_data['send_recipients']
|
||||
if not raw:
|
||||
return []
|
||||
r = raw.split('\n')
|
||||
res = []
|
||||
if ',' in raw or ';' in raw:
|
||||
if '@' in r[0]:
|
||||
raise ValidationError(_('CSV input needs to contain a header row in the first line.'))
|
||||
dialect = csv.Sniffer().sniff(raw[:1024])
|
||||
reader = csv.DictReader(StringIO(raw), dialect=dialect)
|
||||
if 'email' not in reader.fieldnames:
|
||||
raise ValidationError(_('CSV input needs to contain a field with the header "{header}".').format(header="email"))
|
||||
unknown_fields = [f for f in reader.fieldnames if f not in ('email', 'name', 'tag', 'number')]
|
||||
if unknown_fields:
|
||||
raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0]))
|
||||
for i, row in enumerate(reader):
|
||||
try:
|
||||
EmailValidator()(row['email'])
|
||||
except ValidationError as err:
|
||||
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'])) from err
|
||||
try:
|
||||
res.append(self.Recipient(
|
||||
name=row.get('name', ''),
|
||||
email=row['email'],
|
||||
number=int(row.get('number', 1)),
|
||||
tag=row.get('tag', None)
|
||||
))
|
||||
except ValueError as err:
|
||||
raise ValidationError(_('Invalid value in row {number}.').format(number=i + 1)) from err
|
||||
else:
|
||||
for e in r:
|
||||
try:
|
||||
EmailValidator()(e.strip())
|
||||
except ValidationError as err:
|
||||
raise ValidationError(_('{value} is not a valid email address.').format(value=e.strip())) from err
|
||||
else:
|
||||
res.append(self.Recipient(email=e, number=1, tag=None, name=''))
|
||||
return res
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
@@ -222,6 +321,16 @@ class VoucherBulkForm(VoucherForm):
|
||||
if vouchers.exists():
|
||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||
|
||||
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
|
||||
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
|
||||
|
||||
if data.get('codes') and data.get('send'):
|
||||
recp = self.cleaned_data.get('send_recipients', [])
|
||||
code_len = len(data.get('codes'))
|
||||
recp_len = sum(r.number for r in recp)
|
||||
if code_len != recp_len:
|
||||
raise ValidationError(_('You generated {codes} vouchers, but entered recipients for {recp} vouchers.').format(codes=code_len, recp=recp_len))
|
||||
|
||||
return data
|
||||
|
||||
def save(self, event, *args, **kwargs):
|
||||
|
||||
@@ -244,6 +244,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
|
||||
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
|
||||
'pretix.voucher.added': _('The voucher has been created.'),
|
||||
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
|
||||
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
|
||||
'pretix.voucher.changed': _('The voucher has been changed.'),
|
||||
'pretix.voucher.deleted': _('The voucher has been deleted.'),
|
||||
|
||||
@@ -74,6 +74,13 @@
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Send out emails" %}</legend>
|
||||
{% bootstrap_field form.send layout="control" %}
|
||||
{% bootstrap_field form.send_subject layout="horizontal" %}
|
||||
{% bootstrap_field form.send_message layout="horizontal" %}
|
||||
{% bootstrap_field form.send_recipients layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -21,6 +21,7 @@ from django.views.generic import (
|
||||
|
||||
from pretix.base.models import CartPosition, LogEntry, OrderPosition, Voucher
|
||||
from pretix.base.models.vouchers import _generate_random_code
|
||||
from pretix.base.services.vouchers import vouchers_send
|
||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
@@ -314,12 +315,26 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
form.save(self.request.event)
|
||||
# We need to query them again as form.save() uses bulk_create which does not fill in .pk values on databases
|
||||
# other than PostgreSQL
|
||||
voucherids = []
|
||||
for v in self.request.event.vouchers.filter(code__in=form.cleaned_data['codes']):
|
||||
log_entries.append(
|
||||
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False)
|
||||
)
|
||||
voucherids.append(v.pk)
|
||||
LogEntry.objects.bulk_create(log_entries)
|
||||
messages.success(self.request, _('The new vouchers have been created.'))
|
||||
|
||||
if form.cleaned_data['send']:
|
||||
vouchers_send.apply_async(kwargs={
|
||||
'event': self.request.event.pk,
|
||||
'vouchers': voucherids,
|
||||
'subject': form.cleaned_data['send_subject'],
|
||||
'message': form.cleaned_data['send_message'],
|
||||
'recipients': [r._asdict() for r in form.cleaned_data['send_recipients']],
|
||||
'user': self.request.user.pk,
|
||||
})
|
||||
messages.success(self.request, _('The new vouchers have been created and will be sent out shortly.'))
|
||||
else:
|
||||
messages.success(self.request, _('The new vouchers have been created.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_form_class(self):
|
||||
|
||||
Reference in New Issue
Block a user