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 %} +
+
+
+
+ {% csrf_token %} +
+ {% bootstrap_form form layout="inline" %} +
+
+ +
+
+
+{% 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)