Allow to hide payment methods behind a secret link

This commit is contained in:
Raphael Michel
2020-02-05 18:09:27 +01:00
parent b387fba5f4
commit e83b8ac218
4 changed files with 92 additions and 2 deletions
+40 -2
View File
@@ -1,3 +1,4 @@
import hashlib
import json import json
import logging import logging
from collections import OrderedDict from collections import OrderedDict
@@ -14,6 +15,7 @@ from django.dispatch import receiver
from django.forms import Form from django.forms import Form
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries import Countries from django_countries import Countries
@@ -32,7 +34,7 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.views import get_cart, get_cart_total from pretix.presale.views import get_cart, get_cart_total
from pretix.presale.views.cart import cart_session, get_or_create_cart_id from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -204,6 +206,13 @@ class BasePaymentProvider:
implementation. implementation.
""" """
places = settings.CURRENCY_PLACES.get(self.event.currency, 2) places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
if not self.settings.get('_hidden_seed'):
self.settings.set('_hidden_seed', get_random_string(64))
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
})
d = OrderedDict([ d = OrderedDict([
('_enabled', ('_enabled',
forms.BooleanField( forms.BooleanField(
@@ -297,7 +306,28 @@ class BasePaymentProvider:
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
help_text=_( help_text=_(
'Only allow the usage of this payment provider in the following sales channels'), 'Only allow the usage of this payment provider in the following sales channels'),
)) )),
('_hidden',
forms.BooleanField(
label=_('Hide payment method'),
help_text=_(
'The payment method will not be shown by default but only to people who enter the shop through '
'a special link.'
),
)),
('_hidden_url',
forms.URLField(
label=_('Link to enable payment method'),
widget=forms.TextInput(attrs={
'readonly': 'readonly',
'data-display-dependency': '#id_%s_hidden' % self.settings.get_prefix(),
'value': hidden_url,
}),
initial=hidden_url,
help_text=_(
'Share this link with customers who should use this payment method.'
),
)),
]) ])
d['_restricted_countries']._as_type = list d['_restricted_countries']._as_type = list
d['_restrict_to_sales_channels']._as_type = list d['_restrict_to_sales_channels']._as_type = list
@@ -433,6 +463,11 @@ class BasePaymentProvider:
if self.settings._total_min is not None: if self.settings._total_min is not None:
pricing = pricing and total >= Decimal(self.settings._total_min) pricing = pricing and total >= Decimal(self.settings._total_min)
if self.settings._hidden:
hashes = request.session.get('pretix_unlock_hashes', [])
if hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest() not in hashes:
return False
def get_invoice_address(): def get_invoice_address():
if not hasattr(request, '_checkout_flow_invoice_address'): if not hasattr(request, '_checkout_flow_invoice_address'):
cs = cart_session(request) cs = cart_session(request)
@@ -602,6 +637,9 @@ class BasePaymentProvider:
if self.settings._total_min is not None and ps < Decimal(self.settings._total_min): if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
return False return False
if self.settings._hidden:
return False
restricted_countries = self.settings.get('_restricted_countries', as_type=list) restricted_countries = self.settings.get('_restricted_countries', as_type=list)
if restricted_countries: if restricted_countries:
try: try:
+2
View File
@@ -53,6 +53,8 @@ event_patterns = [
csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()), csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()),
name='event.cart.add'), name='event.cart.add'),
url(r'unlock/(?P<hash>[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
name='event.payment.unlock'),
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(), url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/open/(?P<hash>[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(),
+11
View File
@@ -5,6 +5,7 @@ from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.generic import TemplateView from django.views.generic import TemplateView
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
@@ -60,3 +61,13 @@ class ResendLinkView(EventViewMixin, TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['form'] = self.link_form context['form'] = self.link_form
return context return context
class UnlockHashView(EventViewMixin, View):
# Allows to register an unlock hash in the user's session, e.g. to unlock a hidden payment provider
def get(self, request, *args, **kwargs):
hashes = request.session.get('pretix_unlock_hashes', [])
hashes.append(kwargs.get('hash'))
request.session['pretix_unlock_hashes'] = hashes
return redirect(eventreverse(self.request.event, 'presale:event.index'))
+39
View File
@@ -1,4 +1,5 @@
import datetime import datetime
import hashlib
import json import json
import os import os
from datetime import timedelta from datetime import timedelta
@@ -9,6 +10,7 @@ from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -701,6 +703,43 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
doc = BeautifulSoup(response.rendered_content, "lxml") doc = BeautifulSoup(response.rendered_content, "lxml")
assert doc.select(".alert-danger") assert doc.select(".alert-danger")
def test_payment_hidden(self):
self.event.settings.set('payment_stripe__enabled', True)
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.set('payment_banktransfer__hidden', True)
self.event.settings.set('payment_banktransfer__hidden_seed', get_random_string(32))
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select('input[name="payment"]')), 1)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'banktransfer'
}, follow=True)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert doc.select(".alert-danger")
self.client.get('/%s/%s/unlock/%s/' % (
self.orga.slug, self.event.slug,
hashlib.sha256(
(self.event.settings.payment_banktransfer__hidden_seed + self.event.slug).encode()
).hexdigest(),
), follow=True)
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select('input[name="payment"]')), 2)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'banktransfer'
}, follow=True)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert not doc.select(".alert-danger")
def test_payment_min_value(self): def test_payment_min_value(self):
self.event.settings.set('payment_stripe__enabled', True) self.event.settings.set('payment_stripe__enabled', True)
self.event.settings.set('payment_banktransfer__total_min', Decimal('42.00')) self.event.settings.set('payment_banktransfer__total_min', Decimal('42.00'))