From de0e700fec2b9875fa640eb16404a58604452704 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 15 May 2019 08:21:08 +0200 Subject: [PATCH] Store whether we know email addresses are working because links have been clicked --- src/pretix/api/views/order.py | 1 + .../0121_order_email_known_to_work.py | 18 ++++++++++ src/pretix/base/models/orders.py | 18 +++++++--- src/pretix/base/services/mail.py | 5 +-- src/pretix/base/services/orders.py | 35 +++++++++++-------- .../pretixbase/email/plainwrapper.html | 2 +- src/pretix/control/forms/orders.py | 2 +- .../templates/pretixcontrol/order/index.html | 5 ++- src/pretix/control/views/orders.py | 6 ++-- src/pretix/plugins/banktransfer/tasks.py | 5 +-- src/pretix/plugins/sendmail/views.py | 5 +-- src/pretix/presale/urls.py | 2 ++ src/pretix/presale/views/order.py | 9 +++++ src/tests/presale/test_orders.py | 15 ++++++++ 14 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 src/pretix/base/migrations/0121_order_email_known_to_work.py diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 887e1d059c..fdb076d849 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -482,6 +482,7 @@ class OrderViewSet(viewsets.ModelViewSet): ) if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'): + serializer.instance.email_known_to_work = False serializer.instance.log_action( 'pretix.event.order.contact.changed', user=self.request.user, diff --git a/src/pretix/base/migrations/0121_order_email_known_to_work.py b/src/pretix/base/migrations/0121_order_email_known_to_work.py new file mode 100644 index 0000000000..5d5e90b5a9 --- /dev/null +++ b/src/pretix/base/migrations/0121_order_email_known_to_work.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2019-05-15 05:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0120_auto_20190509_0736'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='email_known_to_work', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 567ad297c0..2a963b46d2 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1,4 +1,5 @@ import copy +import hashlib import json import logging import os @@ -180,6 +181,10 @@ class Order(LockModel, LoggedModel): default=False ) sales_channel = models.CharField(max_length=190, default="web") + email_known_to_work = models.BooleanField( + default=False, + verbose_name=_('E-mail address verified') + ) class Meta: verbose_name = _("Order") @@ -206,6 +211,9 @@ class Order(LockModel, LoggedModel): self.event.cache.delete('complain_testmode_orders') self.delete() + def email_confirm_hash(self): + return hashlib.sha256(settings.SECRET_KEY + self.secret.encode()).hexdigest()[:9] + @property def fees(self): """ @@ -729,9 +737,10 @@ class Order(LockModel, LoggedModel): email_template = self.event.settings.mail_text_resend_link email_context = { 'event': self.event.name, - 'url': build_absolute_uri(self.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={ 'order': self.code, - 'secret': self.secret + 'secret': self.secret, + 'hash': self.email_confirm_hash() }), 'invoice_name': invoice_name, 'invoice_company': invoice_company, @@ -1259,9 +1268,10 @@ class OrderPayment(models.Model): email_template = self.order.event.settings.mail_text_order_paid email_context = { 'event': self.order.event.name, - 'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={ 'order': self.order.code, - 'secret': self.order.secret + 'secret': self.order.secret, + 'hash': self.order.email_confirm_hash() }), 'downloads': self.order.event.settings.get('ticket_download', as_type=bool), 'invoice_name': invoice_name, diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index e08634f60a..d2d42a2e4a 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -142,9 +142,10 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], "You can view your order details at the following URL:\n{orderurl}." ).replace("\n", "\r\n").format( event=event.name, orderurl=build_absolute_uri( - order.event, 'presale:event.order', kwargs={ + order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() } ) ) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 528717de07..ee4b4756bd 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -222,9 +222,10 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False 'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency), 'date': LazyDate(order.expires), 'event': order.event.name, - 'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }), 'invoice_name': invoice_name, 'invoice_company': invoice_company, @@ -282,9 +283,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): 'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency), 'date': LazyDate(order.expires), 'event': order.event.name, - 'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }), 'comment': comment, 'invoice_name': invoice_name, @@ -375,9 +377,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device email_context = { 'event': order.event.name, 'code': order.code, - 'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }) } with language(order.locale): @@ -730,9 +733,10 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], 'total_with_currency': LazyCurrencyNumber(order.total, event.currency), 'date': LazyDate(order.expires), 'event': event.name, - 'url': build_absolute_uri(event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }), 'payment_info': payment_info, 'invoice_name': invoice_name, @@ -800,9 +804,10 @@ def send_expiry_warnings(sender, **kwargs): email_template = eventsettings.mail_text_order_expire_warning email_context = { 'event': o.event.name, - 'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={ 'order': o.code, - 'secret': o.secret + 'secret': o.secret, + 'hash': o.email_confirm_hash() }), 'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'), 'invoice_name': invoice_name, @@ -851,9 +856,10 @@ def send_download_reminders(sender, **kwargs): email_template = e.settings.mail_text_download_reminder email_context = { 'event': o.event.name, - 'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={ 'order': o.code, - 'secret': o.secret + 'secret': o.secret, + 'hash': o.email_confirm_hash() }), } email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code} @@ -1350,9 +1356,10 @@ class OrderChangeManager: email_template = order.event.settings.mail_text_order_changed email_context = { 'event': order.event.name, - 'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }), 'invoice_name': invoice_name, 'invoice_company': invoice_company, diff --git a/src/pretix/base/templates/pretixbase/email/plainwrapper.html b/src/pretix/base/templates/pretixbase/email/plainwrapper.html index 264dac759a..f1ff19a682 100644 --- a/src/pretix/base/templates/pretixbase/email/plainwrapper.html +++ b/src/pretix/base/templates/pretixbase/email/plainwrapper.html @@ -27,7 +27,7 @@ {% trans "Event:" %} {{ event.name }}
{% trans "Order code:" %} {{ order.code }}
{% trans "Order date:" %} {{ order.datetime|date:"SHORT_DATE_FORMAT" }}
- + {% trans "View order details" %} diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 4d11a05eeb..320f811005 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -340,7 +340,7 @@ class OrderContactForm(forms.ModelForm): class Meta: model = Order - fields = ['email'] + fields = ['email', 'email_known_to_work'] class OrderLocaleForm(forms.ModelForm): diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 185400bef0..55a63d27df 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -136,7 +136,10 @@ {% endif %}
{% trans "User" %}
- {{ order.email|default_if_none:"" }}   + {{ order.email|default_if_none:"" }} + {% if order.email and order.email_known_to_work %} + + {% endif %}   diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 7f2912e909..b89c602488 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1365,6 +1365,7 @@ class OrderContactChange(OrderView): }, user=self.request.user, ) + if self.form.cleaned_data['regenerate_secrets']: changed = True self.order.secret = generate_secret() @@ -1474,9 +1475,10 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView): '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={ + 'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={ 'order': order.code, - 'secret': order.secret + 'secret': order.secret, + 'hash': order.email_confirm_hash() }), 'invoice_name': invoice_name, 'invoice_company': invoice_company, diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py index 3e5b26ce52..8d19e6fa1e 100644 --- a/src/pretix/plugins/banktransfer/tasks.py +++ b/src/pretix/plugins/banktransfer/tasks.py @@ -38,9 +38,10 @@ def notify_incomplete_payment(o: Order): email_template = o.event.settings.mail_text_order_expire_warning email_context = { 'event': o.event.name, - 'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={ + 'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={ 'order': o.code, - 'secret': o.secret + 'secret': o.secret, + 'hash': o.email_confirm_hash() }), 'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'), 'invoice_name': invoice_name, diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py index e5703db023..6f3b2b5862 100644 --- a/src/pretix/plugins/sendmail/views.py +++ b/src/pretix/plugins/sendmail/views.py @@ -95,9 +95,10 @@ class SenderView(EventPermissionRequiredMixin, FormView): 'event': self.request.event.name, '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={ + 'url': build_absolute_uri(self.request.event, 'presale:event.order.open', kwargs={ 'order': 'ORDER1234', - 'secret': 'longrandomsecretabcdef123456' + 'secret': 'longrandomsecretabcdef123456', + 'hash': 'abcdef', }), 'invoice_name': _('John Doe'), 'invoice_company': _('Sample Company LLC') diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index dca48ba1c4..7c2c468fde 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -49,6 +49,8 @@ event_patterns = [ name='event.cart.add'), url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'), + url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/open/(?P[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(), + name='event.order.open'), 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/order.py b/src/pretix/presale/views/order.py index 6834534875..955b11dc82 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -65,6 +65,15 @@ class OrderDetailMixin(NoSearchIndexViewMixin): }) +@method_decorator(xframe_options_exempt, 'dispatch') +class OrderOpen(EventViewMixin, OrderDetailMixin, View): + def dispatch(self, request, *args, **kwargs): + if kwargs.get('hash') == self.order.email_confirm_hash(): + self.order.email_known_to_work = True + self.order.save(update_fields=['email_known_to_work']) + return redirect(self.get_order_url()) + + @method_decorator(xframe_options_exempt, 'dispatch') class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView): template_name = "pretixpresale/event/order.html" diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py index bc41c9d689..f185638270 100644 --- a/src/tests/presale/test_orders.py +++ b/src/tests/presale/test_orders.py @@ -130,6 +130,21 @@ class OrdersTest(TestCase): ) assert response.status_code == 404 + def test_orders_confirm_email(self): + response = self.client.get( + '/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, 'aabbccdd') + ) + assert response.status_code == 302 + self.order.refresh_from_db() + assert not self.order.email_known_to_work + + response = self.client.get( + '/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, self.order.email_confirm_hash()) + ) + assert response.status_code == 302 + self.order.refresh_from_db() + assert self.order.email_known_to_work + def test_orders_detail(self): response = self.client.get( '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)