mirror of
https://github.com/pretix/pretix.git
synced 2026-09-15 16:34:42 +00:00
Store whether we know email addresses are working because links have been clicked
This commit is contained in:
@@ -482,6 +482,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
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(
|
serializer.instance.log_action(
|
||||||
'pretix.event.order.contact.changed',
|
'pretix.event.order.contact.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -180,6 +181,10 @@ class Order(LockModel, LoggedModel):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
sales_channel = models.CharField(max_length=190, default="web")
|
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:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -206,6 +211,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
self.event.cache.delete('complain_testmode_orders')
|
self.event.cache.delete('complain_testmode_orders')
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
def email_confirm_hash(self):
|
||||||
|
return hashlib.sha256(settings.SECRET_KEY + self.secret.encode()).hexdigest()[:9]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fees(self):
|
def fees(self):
|
||||||
"""
|
"""
|
||||||
@@ -729,9 +737,10 @@ class Order(LockModel, LoggedModel):
|
|||||||
email_template = self.event.settings.mail_text_resend_link
|
email_template = self.event.settings.mail_text_resend_link
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': self.event.name,
|
'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,
|
'order': self.code,
|
||||||
'secret': self.secret
|
'secret': self.secret,
|
||||||
|
'hash': self.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
@@ -1259,9 +1268,10 @@ class OrderPayment(models.Model):
|
|||||||
email_template = self.order.event.settings.mail_text_order_paid
|
email_template = self.order.event.settings.mail_text_order_paid
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': self.order.event.name,
|
'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,
|
'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),
|
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
|
|||||||
@@ -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}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
event=event.name, orderurl=build_absolute_uri(
|
event=event.name, orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order', kwargs={
|
order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||||
'date': LazyDate(order.expires),
|
'date': LazyDate(order.expires),
|
||||||
'event': order.event.name,
|
'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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'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),
|
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||||
'date': LazyDate(order.expires),
|
'date': LazyDate(order.expires),
|
||||||
'event': order.event.name,
|
'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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'comment': comment,
|
'comment': comment,
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -375,9 +377,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
email_context = {
|
email_context = {
|
||||||
'event': order.event.name,
|
'event': order.event.name,
|
||||||
'code': order.code,
|
'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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
with language(order.locale):
|
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),
|
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
|
||||||
'date': LazyDate(order.expires),
|
'date': LazyDate(order.expires),
|
||||||
'event': event.name,
|
'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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'payment_info': payment_info,
|
'payment_info': payment_info,
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -800,9 +804,10 @@ def send_expiry_warnings(sender, **kwargs):
|
|||||||
email_template = eventsettings.mail_text_order_expire_warning
|
email_template = eventsettings.mail_text_order_expire_warning
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': o.event.name,
|
'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,
|
'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'),
|
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -851,9 +856,10 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
email_template = e.settings.mail_text_download_reminder
|
email_template = e.settings.mail_text_download_reminder
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': o.event.name,
|
'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,
|
'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}
|
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_template = order.event.settings.mail_text_order_changed
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': order.event.name,
|
'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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
||||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
||||||
<a href="{% abseventurl event "presale:event.order" order=order.code secret=order.secret %}">
|
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
|
||||||
{% trans "View order details" %}
|
{% trans "View order details" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ class OrderContactForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['email']
|
fields = ['email', 'email_known_to_work']
|
||||||
|
|
||||||
|
|
||||||
class OrderLocaleForm(forms.ModelForm):
|
class OrderLocaleForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -136,7 +136,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<dt>{% trans "User" %}</dt>
|
<dt>{% trans "User" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ order.email|default_if_none:"" }}
|
{{ order.email|default_if_none:"" }}
|
||||||
|
{% if order.email and order.email_known_to_work %}
|
||||||
|
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||||
<span class="fa fa-edit"></span>
|
<span class="fa fa-edit"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1365,6 +1365,7 @@ class OrderContactChange(OrderView):
|
|||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.form.cleaned_data['regenerate_secrets']:
|
if self.form.cleaned_data['regenerate_secrets']:
|
||||||
changed = True
|
changed = True
|
||||||
self.order.secret = generate_secret()
|
self.order.secret = generate_secret()
|
||||||
@@ -1474,9 +1475,10 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
|||||||
'code': order.code,
|
'code': order.code,
|
||||||
'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||||
'expire_date': date_format(order.expires, 'SHORT_DATE_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,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ def notify_incomplete_payment(o: Order):
|
|||||||
email_template = o.event.settings.mail_text_order_expire_warning
|
email_template = o.event.settings.mail_text_order_expire_warning
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': o.event.name,
|
'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,
|
'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'),
|
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
|
|||||||
@@ -95,9 +95,10 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
|||||||
'event': self.request.event.name,
|
'event': self.request.event.name,
|
||||||
'date': date_format(now(), 'SHORT_DATE_FORMAT'),
|
'date': date_format(now(), 'SHORT_DATE_FORMAT'),
|
||||||
'expire_date': date_format(now() + timedelta(days=7), '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',
|
'order': 'ORDER1234',
|
||||||
'secret': 'longrandomsecretabcdef123456'
|
'secret': 'longrandomsecretabcdef123456',
|
||||||
|
'hash': 'abcdef',
|
||||||
}),
|
}),
|
||||||
'invoice_name': _('John Doe'),
|
'invoice_name': _('John Doe'),
|
||||||
'invoice_company': _('Sample Company LLC')
|
'invoice_company': _('Sample Company LLC')
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ event_patterns = [
|
|||||||
name='event.cart.add'),
|
name='event.cart.add'),
|
||||||
|
|
||||||
url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
|
url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
|
||||||
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/open/(?P<hash>[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(),
|
||||||
|
name='event.order.open'),
|
||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
||||||
name='event.order'),
|
name='event.order'),
|
||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice$',
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice$',
|
||||||
|
|||||||
@@ -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')
|
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||||
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||||
template_name = "pretixpresale/event/order.html"
|
template_name = "pretixpresale/event/order.html"
|
||||||
|
|||||||
@@ -130,6 +130,21 @@ class OrdersTest(TestCase):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 404
|
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):
|
def test_orders_detail(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||||
|
|||||||
Reference in New Issue
Block a user