diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py
index a73441b04..cba405e4d 100644
--- a/src/pretix/base/services/mail.py
+++ b/src/pretix/base/services/mail.py
@@ -11,6 +11,7 @@ from pretix.base.models import Event, Order
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.base.mail')
+INVALID_ADDRESS = 'invalid-pretix-mail-address'
class TolerantDict(dict):
@@ -53,6 +54,9 @@ def mail(email: str, subject: str, template: str,
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
+ if email == INVALID_ADDRESS:
+ return
+
with language(locale):
if isinstance(template, LazyI18nString):
body = str(template)
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index aedcfc886..288c5487a 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -163,6 +163,18 @@ to your order for {event}.
You can change your order details and view the status of your order at
{url}
+Best regards,
+Your {event} team"""))
+ },
+ 'mail_text_resend_all_links': {
+ 'type': LazyI18nString,
+ 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
+
+somebody requested a list of your orders for {event}.
+The list is as follows:
+
+{orders}
+
Best regards,
Your {event} team"""))
},
diff --git a/src/pretix/presale/forms/user.py b/src/pretix/presale/forms/user.py
new file mode 100644
index 000000000..4a3da4de4
--- /dev/null
+++ b/src/pretix/presale/forms/user.py
@@ -0,0 +1,7 @@
+from django import forms
+
+from django.utils.translation import ugettext_lazy as _
+
+
+class ResendLinkForm(forms.Form):
+ email = forms.EmailField(label=_('E-mail'))
diff --git a/src/pretix/presale/templates/pretixpresale/event/resend_link.html b/src/pretix/presale/templates/pretixpresale/event/resend_link.html
new file mode 100644
index 000000000..4188d8b05
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/resend_link.html
@@ -0,0 +1,31 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}{% trans "Resend order links" %}{% endblock %}
+{% block content %}
+
+ {% trans "Resend order links" %}
+
+
+
+ {% blocktrans trimmed %}
+ If you lost the link to your order or orders, please enter the email address you
+ used for your order. We will send you an email with links to all orders you placed
+ using this email address.
+ {% endblocktrans %}
+
+
+
+{% endblock %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index fc94cb143..4c48d16d8 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -6,6 +6,7 @@ import pretix.presale.views.event
import pretix.presale.views.locale
import pretix.presale.views.order
import pretix.presale.views.organizer
+import pretix.presale.views.user
# This is not a valid Django URL configuration, as the final
# configuration is done by the pretix.multidomain package.
@@ -18,6 +19,7 @@ event_patterns = [
name='event.redeem'),
url(r'^checkout/(?P[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
name='event.checkout'),
+ url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
name='event.order'),
url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/invoice$',
diff --git a/src/pretix/presale/views/user.py b/src/pretix/presale/views/user.py
new file mode 100644
index 000000000..a087aa705
--- /dev/null
+++ b/src/pretix/presale/views/user.py
@@ -0,0 +1,73 @@
+import logging
+
+from django.conf import settings
+from django.contrib import messages
+from django.shortcuts import redirect
+from django.utils.functional import cached_property
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import TemplateView
+
+from pretix.base.services.mail import INVALID_ADDRESS, SendMailException, mail
+from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
+from pretix.presale.forms.user import ResendLinkForm
+from pretix.presale.views import EventViewMixin
+
+
+class ResendLinkView(EventViewMixin, TemplateView):
+ template_name = 'pretixpresale/event/resend_link.html'
+
+ @cached_property
+ def link_form(self):
+ return ResendLinkForm(data=self.request.POST if self.request.method == 'POST' else None)
+
+ def post(self, request, *args, **kwargs):
+ if not self.link_form.is_valid():
+ messages.error(self.request, _('We had difficulties processing your input.'))
+ return self.get(request, *args, **kwargs)
+
+ user = self.link_form.cleaned_data.get('email')
+
+ if settings.HAS_REDIS:
+ from django_redis import get_redis_connection
+ rc = get_redis_connection("redis")
+ if rc.exists('pretix_resend_{}'.format(user)):
+ messages.error(request, _('We already sent you an email in the last 24 hours.'))
+ return redirect('presale:user.resend')
+ else:
+ rc.setex('pretix_resend_{}'.format(user), 3600 * 24, '1')
+
+ orders = self.request.event.orders.filter(email=user)
+ order_context = []
+
+ for order in orders:
+ url = build_absolute_uri(
+ self.request.event,
+ 'presale:event.order',
+ kwargs={'order': order.code, 'secret': order.secret}
+ )
+ order_context.append(' - {} - {}'.format(order, url))
+
+ if not orders:
+ user = INVALID_ADDRESS
+
+ subject = _('Your orders for {}'.format(self.request.event))
+ template = self.request.event.settings.mail_text_resend_all_links
+ context = {
+ 'orders': '\n'.join(order_context),
+ 'event': self.request.event,
+ }
+ try:
+ mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE)
+ except SendMailException:
+ logger = logging.getLogger('pretix.presale.user')
+ logger.exception('A mail resending order links to {} could not be sent.'.format(user))
+ messages.error(self.request, _('We have trouble sending emails right now, please check back later.'))
+ return self.get(request, *args, **kwargs)
+
+ messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.'))
+ return redirect(eventreverse(self.request.event, 'presale:event.index'))
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['form'] = self.link_form
+ return context
diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py
index 43a267483..5097b2a40 100644
--- a/src/tests/presale/test_event.py
+++ b/src/tests/presale/test_event.py
@@ -1,13 +1,14 @@
import datetime
import time
+from django.core import mail
from django.test import TestCase
from django.utils.timezone import now
from tests.base import SoupTest
from pretix.base.models import (
- Event, EventPermission, Item, ItemCategory, ItemVariation, Organizer,
- Quota, User,
+ Event, EventPermission, Item, ItemCategory, ItemVariation, Order,
+ Organizer, Quota, User,
)
@@ -245,3 +246,60 @@ class DeadlineTest(EventTestMixin, TestCase):
}
)
self.assertNotEqual(response.status_code, 403)
+
+
+class TestResendLink(EventTestMixin, SoupTest):
+ def test_no_orders(self):
+ mail.outbox = []
+ url = '/{}/{}/resend/'.format(self.orga.slug, self.event.slug)
+ resp = self.client.post(url, data={'email': 'dummy@dummy.dummy'})
+
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_no_orders_from_user(self):
+ Order.objects.create(
+ code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
+ email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ total=0,
+ )
+ mail.outbox = []
+ url = '/{}/{}/resend/'.format(self.orga.slug, self.event.slug)
+ resp = self.client.post(url, data={'email': 'dummy@dummy.different'})
+
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_one_order(self):
+ Order.objects.create(
+ code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
+ email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ total=0,
+ )
+ mail.outbox = []
+ url = '/{}/{}/resend/'.format(self.orga.slug, self.event.slug)
+ resp = self.client.post(url, data={'email': 'dummy@dummy.dummy'})
+
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertIn('DUMMY1', mail.outbox[0].body)
+
+ def test_multiple_orders(self):
+ Order.objects.create(
+ code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
+ email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ total=0,
+ )
+ Order.objects.create(
+ code='DUMMY2', status=Order.STATUS_PENDING, event=self.event,
+ email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ total=0,
+ )
+ mail.outbox = []
+ url = '/{}/{}/resend/'.format(self.orga.slug, self.event.slug)
+ resp = self.client.post(url, data={'email': 'dummy@dummy.dummy'})
+
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertIn('DUMMY1', mail.outbox[0].body)
+ self.assertIn('DUMMY2', mail.outbox[0].body)