diff --git a/doc/development/api/datasync.rst b/doc/development/api/datasync.rst new file mode 100644 index 0000000000..9ce4832520 --- /dev/null +++ b/doc/development/api/datasync.rst @@ -0,0 +1,61 @@ +.. highlight:: python + :linenothreshold: 5 + +Data sync providers +=================== + +Pretix provides connectivity to many external services through plugins. A common requirement +is unidirectionally sending (order, customer, ticket, ...) data into external systems. +The transfer is usually triggered by signals provided by pretix core (e.g. order_created), +but performed asynchronously. + +Such plugins should use the :class:`OutboundSyncProvider` API to utilize the queueing, retry and mapping mechanisms as well as the user interface for configuration and monitoring. + +An :class:`OutboundSyncProvider` for registering event participants in a mailing list could start +like this, for example: + +.. code-block:: python + + class MyListSyncProvider(OutboundSyncProvider): + identifier = "my_list" + # ... + + + +The plugin must register listeners in `signals.py` for all signals that should to trigger a sync and +within it has to call `MyListSyncProvider.enqueue_order` to enqueue the order for synchronization: + +.. code-block:: python + + @receiver(order_placed, dispatch_uid="mylist_order_placed") + def on_order_placed(sender, order, **kwargs): + MyListSyncProvider.enqueue_order(order, "order_placed") + + + + +Furthermore, most of these plugins need to transfer data from some pretix objects (e.g. orders) +into an external systems' data structures. Sometimes, there is only one reasonable way or the +plugin author makes an opinionated decision what information from which objects should be +transferred into which data structures in the external system. + +Otherwise, you can use a `PropertyMappingFormSet` to let the user set up a mapping from pretix model fields +to external data fields. You could store the mapping information either in the Event settings, or in a separate +data model. Your implementation of OutboundSyncProvider.mappings needs to provide a list of Mappings, with at least +the properties defined in :class:`pretix.base.datasync.datasync.StaticMapping`. + +.. code-block:: python + + # class MyListSyncProvider, contd. + def mappings(self): + return [ + StaticMapping(1, 'Order', 'Contact', 'email', 'email', + self.event.settings.mylist_order_mapping)) + ] + + +Currently, we support Orders and OrderPositions as data sources, with the data fields defined in +:func:`pretix.base.datasync.sourcefields.get_data_fields`. + + + diff --git a/doc/development/api/index.rst b/doc/development/api/index.rst index eeb7ea445b..e891df35d1 100644 --- a/doc/development/api/index.rst +++ b/doc/development/api/index.rst @@ -18,5 +18,6 @@ Contents: customview cookieconsent auth + datasync general quality diff --git a/src/pretix/base/datasync/datasync.py b/src/pretix/base/datasync/datasync.py index 0fef2943b4..6aba0304ba 100644 --- a/src/pretix/base/datasync/datasync.py +++ b/src/pretix/base/datasync/datasync.py @@ -144,8 +144,19 @@ class OutboundSyncProvider: def display_name(cls): return str(cls.identifier) + """ + Adds an order to the sync queue. May only be called on derived classes which define an "identifier" attribute. + + Should be called in the appropriate signal receivers, e.g.: + + @receiver(order_placed, dispatch_uid="mysync_order_placed") + def on_order_placed(sender, order, **kwargs): + MySyncProvider.enqueue_order(order, "order_placed") + """ @classmethod def enqueue_order(cls, order, triggered_by, not_before=None): + if not hasattr(cls, 'identifier'): + raise TypeError('Call this method on a derived class that defines an "identifier" attribute.') OrderSyncQueue.objects.create( order=order, sync_provider=cls.identifier, @@ -247,6 +258,30 @@ class OutboundSyncProvider: for m in property_mapping ] + """ + This method is called for each object that needs to be created/updated in the external system -- which these are is + determined by the implementation of the `mapping` property. + + TODO: describe the parameters + + This method needs to be idempotent, i.e. calling it multiple times with the same input values should create + only a single object in the target system. + + Subsequent calls with the same mapping and pk_value should update the existing object, instead of creating a new one. + In a SQL database, you might use an "INSERT OR UPDATE" or "UPSERT" statement; many REST APIs provide an equivalent API call. + """ + def sync_object_with_properties( + self, + pk_field, + pk_value, + properties: list, + inputs: dict, + mapping, + mapped_objects: dict, + **kwargs, + ): + raise NotImplementedError() + def sync_object( self, inputs: dict, @@ -261,7 +296,14 @@ class OutboundSyncProvider: if not pk_value: return None - info = self.sync_object_with_properties(inputs, mapping, mapped_objects, pk_value, properties) + info = self.sync_object_with_properties( + pk_field=mapping.external_pk, + pk_value=pk_value, + properties=properties, + inputs=inputs, + mapping=mapping, + mapped_objects=mapped_objects, + ) OrderSyncLink.objects.create( order=inputs.get(ORDER), order_position=inputs.get(ORDER_POSITION), sync_provider=self.identifier, external_object_type=info.get('object_type'),