Fix #369 -- Connect with PayPal (#1084)

* Connect with PayPal

* PayPal connect code-review fixes

* PayPal Connect: Global Env selection; Fix for payee-dict

* Fix missing PayPal Connect indicator for Endpoint

* Fix backwards compatibility
This commit is contained in:
Martin Gross
2018-11-21 11:14:33 +01:00
committed by Raphael Michel
parent a3489eea04
commit a3a63def55
4 changed files with 198 additions and 29 deletions

View File

@@ -9,12 +9,16 @@ from django.contrib import messages
from django.core import signing
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
from django.utils.http import urlquote
from django.utils.translation import ugettext as __, ugettext_lazy as _
from paypalrestsdk.openid_connect import Tokeninfo
from pretix.base.decimal import round_decimal
from pretix.base.models import OrderPayment, OrderRefund, Quota
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -28,19 +32,26 @@ class Paypal(BasePaymentProvider):
payment_form_fields = OrderedDict([
])
def __init__(self, event: Event):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'paypal', event)
@property
def settings_form_fields(self):
d = OrderedDict(
[
('endpoint',
forms.ChoiceField(
label=_('Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
if self.settings.connect_client_id and not self.settings.secret:
# PayPal connect
if self.settings.connect_user_id:
fields = [
('connect_user_id',
forms.CharField(
label=_('PayPal account'),
disabled=True
)),
]
else:
return {}
else:
fields = [
('client_id',
forms.CharField(
label=_('Client ID'),
@@ -56,24 +67,76 @@ class Paypal(BasePaymentProvider):
label=_('Secret'),
max_length=80,
min_length=80,
))
] + list(super().settings_form_fields.items())
)),
('endpoint',
forms.ChoiceField(
label=_('Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
]
d = OrderedDict(
fields + list(super().settings_form_fields.items())
)
d.move_to_end('_enabled', False)
return d
def get_connect_url(self, request):
request.session['payment_paypal_oauth_event'] = request.event.pk
self.init_api()
return Tokeninfo.authorize_url({'scope': 'openid profile email'})
def settings_content_render(self, request):
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
'when payments are refunded externally.'),
build_global_uri('plugins:paypal:webhook')
)
if self.settings.connect_client_id and not self.settings.secret:
# Use PayPal connect
if not self.settings.connect_user_id:
return (
"<p>{}</p>"
"<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
).format(
_('To accept payments via PayPal, you will need an account at PayPal. By clicking on the '
'following button, you can either create a new PayPal account connect pretix to an existing '
'one.'),
self.get_connect_url(request),
_('Connect with {icon} PayPal').format(icon='<i class="fa fa-paypal"></i>')
)
else:
return (
"<button formaction='{}' class='btn btn-danger'>{}</button>"
).format(
reverse('plugins:paypal:oauth.disconnect', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug,
}),
_('Disconnect from PayPal')
)
else:
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
'when payments are refunded externally.'),
build_global_uri('plugins:paypal:webhook')
)
def init_api(self):
paypalrestsdk.set_config(
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
client_id=self.settings.get('client_id'),
client_secret=self.settings.get('secret'))
if self.settings.connect_client_id:
paypalrestsdk.set_config(
mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
client_id=self.settings.connect_client_id,
client_secret=self.settings.connect_secret_key,
openid_client_id=self.settings.connect_client_id,
openid_client_secret=self.settings.connect_secret_key,
openid_redirect_uri=urlquote(build_global_uri('plugins:paypal:oauth.return')))
else:
paypalrestsdk.set_config(
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
client_id=self.settings.get('client_id'),
client_secret=self.settings.get('secret'))
def payment_is_valid_session(self, request):
return (request.session.get('payment_paypal_id', '') != ''
@@ -90,6 +153,18 @@ class Paypal(BasePaymentProvider):
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
if request.event.settings.payment_paypal_connect_user_id:
userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
payee = {
"email": request.event.settings.payment_paypal_connect_user_id,
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
# we should use it instead of the merchant's eMail-address
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
}
else:
payee = {}
payment = paypalrestsdk.Payment({
'intent': 'sale',
'payer': {
@@ -115,7 +190,8 @@ class Paypal(BasePaymentProvider):
"currency": request.event.currency,
"total": self.format_price(cart['total'])
},
"description": __('Event tickets for {event}').format(event=request.event.name)
"description": __('Event tickets for {event}').format(event=request.event.name),
"payee": payee
}
]
})
@@ -333,6 +409,19 @@ class Paypal(BasePaymentProvider):
def payment_prepare(self, request, payment_obj):
self.init_api()
if request.event.settings.payment_paypal_connect_user_id:
userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
payee = {
"email": request.event.settings.payment_paypal_connect_user_id,
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
# we should use it instead of the merchant's eMail-address
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
}
else:
payee = {}
payment = paypalrestsdk.Payment({
'intent': 'sale',
'payer': {
@@ -362,7 +451,8 @@ class Paypal(BasePaymentProvider):
"description": __('Order {order} for {event}').format(
event=request.event.name,
order=payment_obj.order.code
)
),
"payee": payee
}
]
})

View File

@@ -1,11 +1,14 @@
import json
from collections import OrderedDict
from django import forms
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import (
logentry_display, register_payment_providers, requiredaction_display,
logentry_display, register_global_settings, register_payment_providers,
requiredaction_display,
)
@@ -53,3 +56,25 @@ def pretixcontrol_action_display(sender, action, request, **kwargs):
ctx = {'data': data, 'event': sender, 'action': action}
return template.render(ctx, request)
@receiver(register_global_settings, dispatch_uid='paypal_global_settings')
def register_global_settings(sender, **kwargs):
return OrderedDict([
('payment_paypal_connect_client_id', forms.CharField(
label=_('PayPal Connect: Client ID'),
required=False,
)),
('payment_paypal_connect_secret_key', forms.CharField(
label=_('PayPal Connect: Secret key'),
required=False,
)),
('payment_paypal_connect_endpoint', forms.ChoiceField(
label=_('PayPal Connect Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
])

View File

@@ -2,7 +2,9 @@ from django.conf.urls import include, url
from pretix.multidomain import event_url
from .views import abort, redirect_view, success, webhook
from .views import (
abort, oauth_disconnect, oauth_return, redirect_view, success, webhook,
)
event_patterns = [
url(r'^paypal/', include([
@@ -19,5 +21,8 @@ event_patterns = [
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
oauth_disconnect, name='oauth.disconnect'),
url(r'^_paypal/webhook/$', webhook, name='webhook'),
url(r'^_paypal/oauth_return/$', oauth_return, name='oauth.return'),
]

View File

@@ -7,14 +7,17 @@ from django.contrib import messages
from django.core import signing
from django.db.models import Sum
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from paypalrestsdk.openid_connect import Tokeninfo
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import PaymentException
from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal.models import ReferencedPayPalObject
from pretix.plugins.paypal.payment import Paypal
@@ -37,6 +40,37 @@ def redirect_view(request, *args, **kwargs):
return r
def oauth_return(request, *args, **kwargs):
if 'payment_paypal_oauth_event' not in request.session:
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
return redirect(reverse('control:index'))
event = get_object_or_404(Event, pk=request.session['payment_paypal_oauth_event'])
prov = Paypal(event)
prov.init_api()
try:
tokeninfo = Tokeninfo.create(request.GET.get('code'))
userinfo = Tokeninfo.create_with_refresh_token(tokeninfo['refresh_token']).userinfo()
except:
logger.exception('Failed to obtain OAuth token')
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
else:
messages.success(request,
_('Your PayPal account is now connected to pretix. You can change the settings in '
'detail below.'))
event.settings.payment_paypal_connect_refresh_token = tokeninfo['refresh_token']
event.settings.payment_paypal_connect_user_id = userinfo.email
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
'provider': 'paypal'
}))
def success(request, *args, **kwargs):
pid = request.GET.get('paymentId')
token = request.GET.get('token')
@@ -201,3 +235,18 @@ def webhook(request, *args, **kwargs):
pass
return HttpResponse(status=200)
@event_permission_required('can_change_event_settings')
@require_POST
def oauth_disconnect(request, **kwargs):
del request.event.settings.payment_paypal_connect_refresh_token
del request.event.settings.payment_paypal_connect_user_id
request.event.settings.payment_paypal__enabled = False
messages.success(request, _('Your PayPal account has been disconnected.'))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
'provider': 'paypal'
}))