Documentation for the payment provider plugin API

This commit is contained in:
Raphael Michel
2015-03-15 19:48:42 +01:00
parent 41f816388b
commit 13f88346d4
6 changed files with 274 additions and 69 deletions

View File

@@ -19,7 +19,11 @@ import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../src'))
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
django.setup()
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------

View File

@@ -8,21 +8,119 @@ In this document, we will walk through the creation of a payment provider plugin
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already. Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
The signal Provider registration
---------- ---------------------
The payment provider API does not make a lot of usage from signals, however, it The payment provider API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available payment providers. Your plugin does use a signal to get a list of all available payment providers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.payment.PaymentProvider`` should listen for this signal and return the subclass of ``pretix.base.payment.BasePaymentProvider``
that we'll soon create:: that we'll soon create::
from django.dispatch import receiver from django.dispatch import receiver
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from .payment import BankTransfer from .payment import Paypal
@receiver(register_payment_providers) @receiver(register_payment_providers)
def register_payment_provider(sender, **kwargs): def register_payment_provider(sender, **kwargs):
return BankTransfer return Paypal
The provider class
------------------
.. class:: pretix.base.payment.BasePaymentProvider
The central object of each payment provider is the subclass of ``BasePaymentProvider``
we already mentioned above. In this section, we will discuss it's interface in detail.
.. py:attribute:: BasePaymentProvider.event
The default constructor sets this property to the event we are currently
working for.
.. py:attribute:: BasePaymentProvider.settings
The default constructor sets this property to a ``SettingsSandbox`` object. You can
use this object to store settings using its ``get`` and ``set`` methods. All settings
you store are transparently prefixed, so you get your very own settings namespace.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. autoattribute:: is_enabled
.. automethod:: calculate_fee
.. autoattribute:: settings_form_fields
.. automethod:: checkout_form_render
.. automethod:: checkout_form
.. autoattribute:: checkout_form_fields
.. automethod:: checkout_prepare
.. automethod:: checkout_is_valid_session
.. automethod:: checkout_confirm_render
This is an abstract method, you **must** override this!
.. automethod:: checkout_perform
.. automethod:: order_pending_render
This is an abstract method, you **must** override this!
.. automethod:: order_paid_render
Additional views
----------------
For most simple payment providers it is more than sufficient to implement
some of the :py:class:`BasePaymentProvider` methods. However, in some cases
it is necessary to introduce additional views. One example is the PayPal
provider. It redirects the user to a paypal website in the
:py:meth:`BasePaymentProvider.checkout_prepare`` step of the checkout process
and provides PayPal with an URL to redirect back to. This URL points to a
view which looks roughly like this::
@login_required
def success(request):
pid = request.GET.get('paymentId')
payer = request.GET.get('PayerID')
# We stored some information in the session in checkout_prepare(),
# let's compare the new information to double-check that this is about
# the same payment
if pid == request.session['payment_paypal_id']:
# Save the new information to the user's session
request.session['payment_paypal_payer'] = payer
try:
# Redirect back to the confirm page. We chose to save the
# event ID in the user's session. We could also put this
# information into an URL parameter.
event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
return redirect(reverse('presale:event.checkout.confirm', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}))
except Event.DoesNotExist:
pass # TODO: Display error message
else:
pass # TODO: Display error message
If you do not want to provide a view of your own, you could even let PayPal
redirect directly back to the confirm page and handle the query parameters
inside :py:meth:`BasePaymentProvider.checkout_is_valid_session``. However,
because some external providers (not PayPal) force you to have a *constant*
redirect URL, it might be necessary to define custom views.

View File

@@ -13,23 +13,45 @@ require two steps to install:
* Add it to the ``INSTALLED_APPS`` setting of Django in ``pretix/settings.py`` * Add it to the ``INSTALLED_APPS`` setting of Django in ``pretix/settings.py``
* Perform database migrations by using ``python manage.py migrate`` * Perform database migrations by using ``python manage.py migrate``
The communication between pretix and the plugins happens via Django's The communication between pretix and the plugins happens mostly using Django's
`signal dispatcher`_ pattern. The core modules of pretix, ``pretixbase``, `signal dispatcher`_ feature. The core modules of pretix, ``pretixbase``,
``pretixcontrol`` and ``pretixpresale`` expose a number of signals which are documented ``pretixcontrol`` and ``pretixpresale`` expose a number of signals which are documented
on the next pages. on the next pages.
.. _`pluginsetup`: .. _`pluginsetup`:
Creating a plugin To create a new plugin, create a new python package which must be a vaild `Django app`_
----------------- and must contain plugin metadata, as described below.
To create a new plugin, create a new python package. The following pages go into detail about the several types of plugins currently
supported. While these instructions don't assume that you know a lot about pretix,
they do assume that you have prior knowledge about Django (e.g. it's view layer,
how it's ORM works, etc.).
Inside your newly created folder, you'll probably need the three python modules ``__init__.py``, Plugin metadata
``models.py`` and ``signals.py``, although this is up to you. You can take the following ---------------
example, taken from the time restriction module (see next chapter) as a template for your
``__init__.py`` module::
The plugin metadata lives inside a ``PretixPluginMeta`` class inside your app's
configuration class. The metadata class must define the following attributes:
``type`` (``pretix.base.plugins.PluginType``):
The type of plugin. Currently available: ``RESTRICTION``, ``PAYMENT``
``name`` (``str``):
The human-readable name of your plugin
``author`` (``str``):
Your name
``version`` (``str``):
A human-readable version code of your plugin
``description`` (``str``):
A more verbose description of what your plugin does.
A working example would be::
# file: pretix/plugins/timerestriction/__init__.py
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.plugins import PluginType from pretix.base.plugins import PluginType
@@ -48,21 +70,36 @@ example, taken from the time restriction module (see next chapter) as a template
"of a given item or variation to a certain timeframe " + "of a given item or variation to a certain timeframe " +
"or change its price during a certain period.") "or change its price during a certain period.")
def ready(self):
from . import signals # NOQA
default_app_config = 'pretix.plugins.timerestriction.TimeRestrictionApp' default_app_config = 'pretix.plugins.timerestriction.TimeRestrictionApp'
.. IMPORTANT::
You have to implement a ``PretixPluginMeta`` class like in the example to make your
plugin available to the users.
Currently, the ``PluginType`` enum has the following values defined: Signals
-------
* ``RESTRICTION`` The various components of pretix define a number of signals which your plugin can
* ``PAYMENT`` listen for. We will go into the details of the different signals in the following
pages. We suggest that you put your signal receivers into a ``signals`` submodule
of your plugin. You should extend your ``AppConfig`` (see above) by the following
method to make your receivers available::
The next pages provide details on their usage. class TimeRestrictionApp(AppConfig):
def ready(self):
from . import signals # NOQA
Views
-----
Your plugin may define custom views. If you put an ``urls`` submodule into your
plugin module, pretix will automatically import it and include it into the root
URL configuration.
.. WARNING:: If you define custom URLs and views, you are currently on your own
with checking that the calling user is logged in, has appropriate permissions,
etc. We plan on providing native support for this in a later version.
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/ .. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/ .. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/

View File

@@ -4,9 +4,9 @@
Writing a restriction plugin Writing a restriction plugin
============================ ============================
Please make sure you have read and understood the :ref:`basic idea being pretix's restrictions Please make sure you have read and understood the :ref:`basic idea <restrictionconcept>` behind
<restrictionconcept>`. In this document, we will walk through the creation of a restriction what pretix calls *restrictions*. In this document, we will walk through the creation of a
plugin using the example of a restriction by date and time. restriction plugin using the example of a restriction by date and time.
Also, read :ref:`Creating a plugin <pluginsetup>` first. Also, read :ref:`Creating a plugin <pluginsetup>` first.

View File

@@ -3,10 +3,12 @@ from decimal import Decimal
from django import forms from django import forms
from django.forms import Form from django.forms import Form
from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import SettingsForm from pretix.base.forms import SettingsForm
from pretix.base.models import Order
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
@@ -66,11 +68,30 @@ class BasePaymentProvider:
def settings_form_fields(self) -> dict: def settings_form_fields(self) -> dict:
""" """
When the event's administrator administrator visits the event configuration When the event's administrator administrator visits the event configuration
page, page, this method is called to return the configuration fields available.
A dictionary. The keys should be (unprefixed) EventSetting keys,
the values should be corresponding django form fields.
We suggest returning a collections.OrderedDict object instead of a dict. It should therefore return a dictionary where the keys should be (unprefixed)
settings keys and the values should be corresponding Django form fields.
The default implementation returns the appropriate fields for the ``_enabled``,
``_fee_abs`` and ``_fee_percent`` settings mentioned above.
We suggest that you return an ``OrderedDict`` object instead of a dictionary
and make use of the default implementation. Your implementation could look
like this::
@property
def settings_form_fields(self):
return OrderedDict(
list(super().settings_form_fields.items()) + [
('bank_details',
forms.CharField(
widget=forms.Textarea,
label=_('Bank account details'),
required=False
))
]
)
""" """
return OrderedDict([ return OrderedDict([
('_enabled', ('_enabled',
@@ -96,18 +117,20 @@ class BasePaymentProvider:
@property @property
def checkout_form_fields(self) -> dict: def checkout_form_fields(self) -> dict:
""" """
A dictionary. The keys should be unprefixed field names, This is used by the default implementation of :py:meth:`checkout_form`.
the values should be corresponding django form fields. It should return an object similar to :py:attr:`settings_form_fields`.
We suggest returning a collections.OrderedDict object instead of a dict. The default implementation returns an empty dictionary.
""" """
# TODO: Proper handling of required=True fields in HTML
return {} return {}
def checkout_form(self, request) -> Form: def checkout_form(self, request: HttpRequest) -> Form:
""" """
Returns the Form object of the form that should be displayed when the This is called by the default implementation of :py:meth:`checkout_form_render`
user selects this provider as his payment method. to obtain the form that is displayed to the user during the checkout
process. The default implementation constructs the form using
:py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
and all fields and fills the form with data form the user's session.
""" """
form = Form( form = Form(
data=(request.POST if request.method == 'POST' else None), data=(request.POST if request.method == 'POST' else None),
@@ -121,10 +144,15 @@ class BasePaymentProvider:
form.fields = self.checkout_form_fields form.fields = self.checkout_form_fields
return form return form
def checkout_form_render(self, request) -> str: def checkout_form_render(self, request: HttpRequest) -> str:
""" """
Returns the HTML of the form that should be displayed when the user When the user selects this provider as his prefered payment method,
selects this provider as his payment method. he will be shown the HTML you return from this method.
The default implementation will call :py:meth:`checkout_form`
and render the returned form. If your payment method doesn't require
the user to fill out form fields, you should just return a paragraph
of explainatory text.
""" """
form = self.checkout_form(request) form = self.checkout_form(request)
template = get_template('pretixpresale/event/checkout_payment_form_default.html') template = get_template('pretixpresale/event/checkout_payment_form_default.html')
@@ -133,23 +161,53 @@ class BasePaymentProvider:
def checkout_confirm_render(self, request) -> str: def checkout_confirm_render(self, request) -> str:
""" """
Returns the HTML that should be displayed when the user selected this provider If the user successfully filled in his payment data, he will be redirected
on the 'confirm order' page. to a confirmation page which lists all details of his order for a final review.
This method should return the HTML which should be displayed inside the
'Payment' box on this page.
In most cases, this should include a short summary of the user's input and
a short explaination on how the payment process will continue.
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
def checkout_prepare(self, request, cart) -> "bool|HttpResponse": def checkout_prepare(self, request: HttpRequest, cart: dict) -> "bool|str":
""" """
Will be called if the user selects this provider as his payment method. Will be called after the user selected this provider as his payment method.
If the payment provider provides a form to the user to enter payment data, If you provided a form to the user to enter payment data, this method should
this method should at least store the user's input into his session. at least store the user's input into his session.
It should return True or False, depending of the validity of the user's input, This method should return ``False``, if the user's input was invalid, ``True``
if the frontend should continue with default behaviour, or a redirect URL, if the input was valid and the frontend should continue with default behaviour
if you need special behaviour. or a string containing an URL, if the user should be redirected somewhere else.
On errors, it should use Django's message framework to display an error message On errors, you should use Django's message framework to display an error message
to the user (or the normal form validation error messages). to the user (or the normal form validation error messages).
The default implementation stores the input into the form returned by
:py:meth:`checkout_form` in the user's session.
If your payment method requires you to redirect the user to an external provider,
this might be the place to do so.
.. IMPORTANT:: If this is called, the user has not yet confirmed his or her order.
You may NOT do anything which actually moves money.
:param cart: This dictionary contains at least the following keys:
positions:
A list of ``CartPosition`` objects that are annotated with the special
attributes ``count`` and ``total`` because multiple objects of the
same content are grouped into one.
raw:
The raw list of ``CartPosition`` objects in the users cart
total:
The overall total *including* the fee for the payment method.
payment_fee:
The fee for the payment method.
""" """
form = self.checkout_form(request) form = self.checkout_form(request)
if form.is_valid(): if form.is_valid():
@@ -159,49 +217,58 @@ class BasePaymentProvider:
else: else:
return False return False
def checkout_is_valid_session(self, request) -> bool: def checkout_is_valid_session(self, request: HttpRequest) -> bool:
""" """
This is called at the time the user tries to place the order. It should return This is called at the time the user tries to place the order. It should return
True, if the user's session is valid and all data your payment provider requires ``True``, if the user's session is valid and all data your payment provider requires
in future steps is present. in future steps is present.
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
def checkout_perform(self, request, order) -> str: def checkout_perform(self, request: HttpRequest, order: Order) -> str:
""" """
Will be called if the user submitted his order successfully to initiate the After the user confirmed his purchase, this method will be called to complete
payment process. the payment process. This is the place to actually move the money, if applicable.
If you need any speical behaviour, you can return a string
containing an URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page.
It should return a custom redirct URL, if you need special behaviour, or None to If the payment is completed, you should call ``order.mark_paid(provider, info)``
continue with default behaviour. with ``provider`` being your :py:attr:`identifier` and ``info`` being any string
you might want to store for later usage. Please note, that if you want to store
something inside ``order.payment_info``, please do a ``order = order.clone()`` before
modifying or saving the order object.
On errors, it should use Django's message framework to display an error message The default implementation just returns ``None`` and therefore leaves the
to the user (or the normal form validation error messages). order unpaid. The user will be redirected to the order's detail page by default.
On errors, you should use Django's message framework to display an error message
to the user.
:param order: The order object :param order: The order object
""" """
return None return None
def order_pending_render(self, request, order) -> str: def order_pending_render(self, request: HttpRequest, order: Order) -> str:
""" """
Will be called if the user views the detail page of an unpaid order which is If the user visits a detail page of an order which has not yet been paid but
associated with this payment provider. this payment method was selected during checkout, this method will be called
to provide HTML content for the 'payment' box on the page.
It should return HTML code which should be displayed to the user. It should contian It should contain instructions on how to continue with the payment process,
instructions on how to continue with the payment process, either in form of text either in form of text or buttons/links/etc.
or buttons/links/etc.
:param order: The order object :param order: The order object
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
def order_paid_render(self, request, order) -> str: def order_paid_render(self, request: HttpRequest, order: Order) -> str:
""" """
Will be called if the user views the detail page of an paid order which is Will be called if the user views the detail page of an paid order which is
associated with this payment provider. associated with this payment provider.
It should return HTML code which should be displayed to the user or None, It should return HTML code which should be displayed to the user or None,
if there is nothing to say. if there is nothing to say (like the default implementation does).
:param order: The order object :param order: The order object
""" """

View File

@@ -5,7 +5,6 @@ from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template import Context from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
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.utils.translation import ugettext as __ from django.utils.translation import ugettext as __
from django import forms from django import forms