diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index ddf9b65812..72ea09ac98 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -346,6 +346,16 @@ your order {code} for {event} has been canceled. You can view the details of your order at {url} +Best regards, +Your {event} team""")) + }, + 'mail_text_order_custom_mail': { + 'type': LazyI18nString, + 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, + +You can change your order details and view the status of your order at +{url} + Best regards, Your {event} team""")) }, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 7d2a83329c..6734e6fd71 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -614,6 +614,15 @@ class MailSettingsForm(SettingsForm): help_text=_("Available placeholders: {event}, {code}, {url}"), validators=[PlaceholderValidator(['{event}', '{code}', '{url}'])] ) + mail_text_order_custom_mail = I18nFormField( + label=_("Text"), + required=False, + widget=I18nTextarea, + help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, " + "{invoice_name}, {invoice_company}"), + validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}', + '{invoice_name}', '{invoice_company}'])] + ) smtp_use_custom = forms.BooleanField( label=_("Use custom SMTP server"), help_text=_("All mail related to your event will be sent over the smtp server specified by you."), diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index bb570b838c..d90d561211 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -6,7 +6,7 @@ from django.utils.formats import localize from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ -from pretix.base.forms import I18nModelForm +from pretix.base.forms import I18nModelForm, PlaceholderValidator from pretix.base.models import Item, ItemAddOn, Order, OrderPosition from pretix.base.models.event import SubEvent from pretix.base.services.pricing import get_price @@ -215,3 +215,30 @@ class OrderLocaleForm(forms.ModelForm): super().__init__(*args, **kwargs) locale_names = dict(settings.LANGUAGES) self.fields['locale'].choices = [(a, locale_names[a]) for a in self.instance.event.settings.locales] + + +class OrderMailForm(forms.Form): + subject = forms.CharField( + label=_("Subject"), + required=True + ) + + def __init__(self, *args, **kwargs): + order = kwargs.pop('order') + super().__init__(*args, **kwargs) + self.fields['sendto'] = forms.EmailField( + label=_("Recipient"), + required=True, + initial=order.email + ) + self.fields['sendto'].widget.attrs['readonly'] = 'readonly' + self.fields['message'] = forms.CharField( + label=_("Message"), + required=True, + widget=forms.Textarea, + initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale), + help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, " + "{invoice_name}, {invoice_company}"), + validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}', + '{invoice_name}', '{invoice_company}'])] + ) diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 51ceb0553b..2a26107ed9 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -107,6 +107,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.order.payment.changed': _('The payment method has been changed.'), 'pretix.event.order.expire_warning_sent': _('An email has been sent with a warning that the order is about ' 'to expire.'), + 'pretix.event.order.mail_sent': _('A custom email has been sent.'), 'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), diff --git a/src/pretix/control/templates/pretixcontrol/event/mail.html b/src/pretix/control/templates/pretixcontrol/event/mail.html index 30db5ae0c9..6966f09f84 100644 --- a/src/pretix/control/templates/pretixcontrol/event/mail.html +++ b/src/pretix/control/templates/pretixcontrol/event/mail.html @@ -39,6 +39,9 @@ {% blocktrans asvar title_order_canceled %}Order canceled{% endblocktrans %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_text_order_canceled" %} + {% blocktrans asvar title_order_custom_mail %}Order custom mail{% endblocktrans %} + {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %} +
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 43af579a87..c22a3400a2 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -42,6 +42,9 @@ class="btn btn-default" target="_blank"> {% trans "View order as user" %} + + {% trans "View email history" %} + @@ -93,6 +96,9 @@ + + + {% if order.status != "c" %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/mail_history.html b/src/pretix/control/templates/pretixcontrol/order/mail_history.html new file mode 100644 index 0000000000..1d0d1d84c4 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/order/mail_history.html @@ -0,0 +1,33 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Email history" %}{% endblock %} +{% block content %} +

{% trans "Email history" %}

+
+ {% trans "Includes sent custom and mass emails history only." %} +
+
+
    + {% for log in logs %} +
  • +

    + {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }} + {% if log.user %} +
    {{ log.user.get_full_name }} + {% endif %} + {% if log.display %} +
    {{ log.display }} + {% endif %} +

    +

    + {% trans "Subject:" %} + {{ log.parsed_data.subject }} +

    {{ log.parsed_data.message }}
    +

    +
  • + {% endfor %} +
+
+ {% include "pretixcontrol/pagination.html" %} +{% endblock %} \ No newline at end of file diff --git a/src/pretix/control/templates/pretixcontrol/order/sendmail.html b/src/pretix/control/templates/pretixcontrol/order/sendmail.html new file mode 100644 index 0000000000..7f0d5842e2 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/order/sendmail.html @@ -0,0 +1,37 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Send email" %}{% endblock %} +{% block content %} +

{% trans "Send email" %}

+ {% block inner %} + + {% csrf_token %} + {% bootstrap_field form.sendto layout='horizontal' %} + {% bootstrap_field form.subject layout='horizontal' %} + {% bootstrap_field form.message layout='horizontal' %} + {% if request.method == "POST" %} +
+ {% trans "E-mail preview" %} +
+
+                            {% for segment in preview_output %}
+                                {% spaceless %}
+                                    {{ segment|linebreaksbr }}
+                                {% endspaceless %}
+                            {% endfor %}
+                        
+
+
+ {% endif %} +
+ + +
+
+ {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 9a1f48ac1a..fb1009daa4 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -138,6 +138,10 @@ urlpatterns = [ name='event.order.comment'), url(r'^orders/(?P[0-9A-Z]+)/change$', orders.OrderChange.as_view(), name='event.order.change'), + url(r'^orders/(?P[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(), + name='event.order.sendmail'), + url(r'^orders/(?P[0-9A-Z]+)/mail_history$', orders.OrderEmailHistory.as_view(), + name='event.order.mail_history'), url(r'^orders/(?P[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'), url(r'^invoice/(?P[^/]+)$', orders.InvoiceDownload.as_view(), name='event.invoice.download'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 71b04b989b..e030bf77ee 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -443,6 +443,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): 'mail_text_order_expire_warning': ['event', 'url', 'expire_date', 'invoice_name', 'invoice_company'], 'mail_text_waiting_list': ['event', 'url', 'product', 'hours', 'code'], 'mail_text_order_canceled': ['code', 'event', 'url'], + 'mail_text_order_custom_mail': ['expire_date', 'event', 'code', 'date', 'url', + 'invoice_name', 'invoice_company'] } @cached_property diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 322db3451b..5d979dfb44 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1,20 +1,25 @@ from datetime import timedelta +import pytz from django.conf import settings from django.contrib import messages from django.core.urlresolvers import reverse -from django.db.models import Count +from django.db.models import Count, Q from django.http import FileResponse, Http404, HttpResponseNotAllowed from django.shortcuts import redirect, render +from django.utils.formats import date_format from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from django.views.generic import DetailView, ListView, TemplateView, View +from django.views.generic import ( + DetailView, FormView, ListView, TemplateView, View, +) +from i18nfield.strings import LazyI18nString from pretix.base.i18n import language from pretix.base.models import ( CachedFile, CachedTicket, Invoice, InvoiceAddress, Item, ItemVariation, - Order, Quota, generate_position_secret, generate_secret, + LogEntry, Order, Quota, generate_position_secret, generate_secret, ) from pretix.base.models.event import SubEvent from pretix.base.services.export import export @@ -33,7 +38,7 @@ from pretix.base.views.async import AsyncAction from pretix.control.forms.filter import EventOrderFilterForm from pretix.control.forms.orders import ( CommentForm, ExporterForm, ExtendForm, OrderContactForm, OrderLocaleForm, - OrderPositionAddForm, OrderPositionChangeForm, + OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm, ) from pretix.control.permissions import EventPermissionRequiredMixin from pretix.multidomain.urlreverse import build_absolute_uri @@ -604,6 +609,114 @@ class OrderLocaleChange(OrderView): return self.get(*args, **kwargs) +class OrderSendMail(EventPermissionRequiredMixin, FormView): + template_name = 'pretixcontrol/order/sendmail.html' + permission = 'can_change_orders' + form_class = OrderMailForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['order'] = Order.objects.get( + event=self.request.event, + code=self.kwargs['code'].upper() + ) + return kwargs + + def form_invalid(self, form): + messages.error(self.request, _('We could not send the email. See below for details.')) + return super().form_invalid(form) + + def form_valid(self, form): + tz = pytz.timezone(self.request.event.settings.timezone) + order = Order.objects.get( + event=self.request.event, + code=self.kwargs['code'].upper() + ) + self.preview_output = {} + try: + invoice_name = order.invoice_address.name + invoice_company = order.invoice_address.company + except InvoiceAddress.DoesNotExist: + invoice_name = "" + invoice_company = "" + with language(order.locale): + email_context = { + 'event': order.event, + 'code': order.code, + 'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'), + 'expire_date': date_format(order.expires, 'SHORT_DATE_FORMAT'), + 'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={ + 'order': order.code, + 'secret': order.secret + }), + 'invoice_name': invoice_name, + 'invoice_company': invoice_company, + } + + email_content = form.cleaned_data['message'].format_map(email_context) + if self.request.POST.get('action') == 'preview': + self.preview_output = [] + self.preview_output.append( + _('Subject: {subject}').format(subject=form.cleaned_data['subject'])) + self.preview_output.append(email_content) + return self.get(self.request, *self.args, **self.kwargs) + else: + try: + with language(order.locale): + email_template = LazyI18nString(form.cleaned_data['message']) + mail( + order.email, form.cleaned_data['subject'], + email_template, email_context, + self.request.event, locale=order.locale, + order=order + ) + order.log_action( + 'pretix.event.order.mail_sent', + user=self.request.user, + data={ + 'subject': form.cleaned_data['subject'], + 'message': email_content, + 'recipient': form.cleaned_data['sendto'], + } + ) + messages.success(self.request, _('Your message has been queued and will be sent to {}.'.format(order.email))) + except SendMailException: + messages.error( + self.request, + _('Failed to send mail to the following user: {}'.format(order.email)) + ) + return super(OrderSendMail, self).form_valid(form) + + def get_success_url(self): + return reverse('control:event.order', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + 'code': self.kwargs['code']} + ) + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['preview_output'] = getattr(self, 'preview_output', None) + return ctx + + +class OrderEmailHistory(EventPermissionRequiredMixin, ListView): + template_name = 'pretixcontrol/order/mail_history.html' + permission = 'can_view_orders' + model = LogEntry + context_object_name = 'logs' + paginate_by = 5 + + def get_queryset(self): + order = Order.objects.filter( + event=self.request.event, + code=self.kwargs['code'].upper() + ).first() + qs = order.all_logentries() + qs = qs.filter(Q(action_type="pretix.plugins.sendmail.order.email.sent") | Q(action_type="pretix.event.order.mail_sent")) + return qs + + class OverView(EventPermissionRequiredMixin, TemplateView): template_name = 'pretixcontrol/orders/overview.html' permission = 'can_view_orders' diff --git a/src/pretix/plugins/sendmail/forms.py b/src/pretix/plugins/sendmail/forms.py index da526f0718..ad35ea55f4 100644 --- a/src/pretix/plugins/sendmail/forms.py +++ b/src/pretix/plugins/sendmail/forms.py @@ -28,9 +28,9 @@ class MailForm(forms.Form): self.fields['message'] = I18nFormField( widget=I18nTextarea, required=True, locales=event.settings.get('locales'), - help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, " + help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, " "{invoice_name}, {invoice_company}"), - validators=[PlaceholderValidator(['{due_date}', '{event}', '{order}', '{order_date}', '{order_url}', + validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}', '{invoice_name}', '{invoice_company}'])] ) choices = list(Order.STATUS_CHOICE) diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py index 8e3a469cc2..307ba14a4b 100644 --- a/src/pretix/plugins/sendmail/views.py +++ b/src/pretix/plugins/sendmail/views.py @@ -84,11 +84,11 @@ class SenderView(EventPermissionRequiredMixin, FormView): _('Subject: {subject}').format(subject=form.cleaned_data['subject'].localize(l))) message = form.cleaned_data['message'].localize(l) preview_text = message.format( - order='ORDER1234', + code='ORDER1234', event=self.request.event.name, - order_date=date_format(now(), 'SHORT_DATE_FORMAT'), - due_date=date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'), - order_url=build_absolute_uri(self.request.event, 'presale:event.order', kwargs={ + date=date_format(now(), 'SHORT_DATE_FORMAT'), + expire_date=date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'), + url=build_absolute_uri(self.request.event, 'presale:event.order', kwargs={ 'order': 'ORDER1234', 'secret': 'longrandomsecretabcdef123456' }), @@ -105,31 +105,31 @@ class SenderView(EventPermissionRequiredMixin, FormView): except InvoiceAddress.DoesNotExist: invoice_name = "" invoice_company = "" - try: with language(o.locale): + email_context = { + 'event': o.event, + 'code': o.code, + 'date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'), + 'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT'), + 'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={ + 'order': o.code, + 'secret': o.secret + }), + 'invoice_name': invoice_name, + 'invoice_company': invoice_company, + } mail( o.email, form.cleaned_data['subject'], form.cleaned_data['message'], - { - 'event': o.event, - 'order': o.code, - 'order_date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'), - 'due_date': date_format(o.expires, 'SHORT_DATE_FORMAT'), - 'order_url': build_absolute_uri(o.event, 'presale:event.order', kwargs={ - 'order': o.code, - 'secret': o.secret - }), - 'invoice_name': invoice_name, - 'invoice_company': invoice_company, - }, + email_context, self.request.event, locale=o.locale, order=o) o.log_action( 'pretix.plugins.sendmail.order.email.sent', user=self.request.user, data={ - 'subject': form.cleaned_data['subject'], - 'message': form.cleaned_data['message'], - 'recipient': o.email, + 'subject': form.cleaned_data['subject'].localize(o.locale), + 'message': form.cleaned_data['message'].localize(o.locale).format_map(email_context), + 'recipient': o.email } ) except SendMailException: diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index 4a2816cf10..10e22e4978 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -452,6 +452,100 @@ def test_order_go_not_found(client, env): assert response['Location'].endswith('/control/event/dummy/dummy/orders/') +@pytest.fixture +def order_url(env): + event = env[0] + order = env[2] + url = '/control/event/{orga}/{event}/orders/{code}'.format( + event=event.slug, orga=event.organizer.slug, code=order.code + ) + return url + + +@pytest.mark.django_db +def test_order_sendmail_view(client, order_url): + client.login(email='dummy@dummy.dummy', password='dummy') + sendmail_url = order_url + '/sendmail' + response = client.get(sendmail_url) + + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_order_sendmail_simple_case(client, order_url, env): + order = env[2] + client.login(email='dummy@dummy.dummy', password='dummy') + sendmail_url = order_url + '/sendmail' + mail.outbox = [] + response = client.post( + sendmail_url, + { + 'sendto': order.email, + 'subject': 'Test subject', + 'message': 'This is a test file for sending mails.' + }, + follow=True) + + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + + assert len(mail.outbox) == 1 + assert mail.outbox[0].to == [order.email] + assert mail.outbox[0].subject == 'Test subject' + assert 'This is a test file for sending mails.' in mail.outbox[0].body + + mail_history_url = order_url + '/mail_history' + response = client.get(mail_history_url) + + assert response.status_code == 200 + assert 'Test subject' in response.rendered_content + + +@pytest.mark.django_db +def test_order_sendmail_preview(client, order_url, env): + order = env[2] + client.login(email='dummy@dummy.dummy', password='dummy') + sendmail_url = order_url + '/sendmail' + mail.outbox = [] + response = client.post( + sendmail_url, + { + 'sendto': order.email, + 'subject': 'Test subject', + 'message': 'This is a test file for sending mails.', + 'action': 'preview' + }, + follow=True) + + assert response.status_code == 200 + assert 'E-mail preview' in response.rendered_content + assert len(mail.outbox) == 0 + + +@pytest.mark.django_db +def test_order_sendmail_invalid_data(client, order_url, env): + order = env[2] + client.login(email='dummy@dummy.dummy', password='dummy') + sendmail_url = order_url + '/sendmail' + mail.outbox = [] + response = client.post( + sendmail_url, + { + 'sendto': order.email, + 'subject': 'Test invalid mail', + }, + follow=True) + + assert 'has-error' in response.rendered_content + assert len(mail.outbox) == 0 + + mail_history_url = order_url + '/mail_history' + response = client.get(mail_history_url) + + assert response.status_code == 200 + assert 'Test invalid mail' not in response.rendered_content + + class OrderChangeTests(SoupTest): def setUp(self): super().setUp() diff --git a/src/tests/plugins/test_sendmail.py b/src/tests/plugins/test_sendmail.py index 23eef55b36..5450c677e9 100644 --- a/src/tests/plugins/test_sendmail.py +++ b/src/tests/plugins/test_sendmail.py @@ -39,7 +39,7 @@ def order(item): o = Order.objects.create(event=item.event, status=Order.STATUS_PENDING, expires=now() + datetime.timedelta(hours=1), total=13, code='DUMMY', email='dummy@dummy.test', - datetime=now(), payment_provider='banktransfer') + datetime=now(), payment_provider='banktransfer', locale='en') OrderPosition.objects.create(order=o, item=item, price=13) return o