diff --git a/doc/development/api/datasync.rst b/doc/development/api/datasync.rst index 02ae9b94d6..bd8d5d8168 100644 --- a/doc/development/api/datasync.rst +++ b/doc/development/api/datasync.rst @@ -55,9 +55,6 @@ needs to provide a list of mappings, with at least the properties defined in ] -.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider - :members: mappings - Currently, we support ``Order`` and ``OrderPosition`` as data sources, with the data fields defined in :func:`pretix.base.datasync.sourcefields.get_data_fields`. @@ -71,9 +68,6 @@ For each order that was enqueued using ``enqueue_order``: - ``finalize_sync_order`` is called one time after all calls to ``sync_object_with_properties``. -.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider - :members: sync_object_with_properties, finalize_sync_order - For example implementations, see the test cases in :package:``tests.base.test_datasync``. In :class:`SimpleOrderSync`, a basic data transfer of order data only is shown. Therein, a ``sync_object_with_properties`` method is defined like as follows: @@ -128,3 +122,6 @@ shown. Therein, a ``sync_object_with_properties`` method is defined like as foll In :class:`OrderAndTicketAssociationSync`, an example is given where orders, order positions, and the association between them are transferred. + +.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider + :members: \ No newline at end of file diff --git a/src/pretix/base/datasync/datasync.py b/src/pretix/base/datasync/datasync.py index 3bb8046787..4bb1285520 100644 --- a/src/pretix/base/datasync/datasync.py +++ b/src/pretix/base/datasync/datasync.py @@ -95,8 +95,8 @@ class OrderSyncLink(models.Model): OrderPosition, on_delete=models.CASCADE, related_name="synced_objects", blank=True, null=True, ) external_object_type = models.CharField(blank=False, null=False, max_length=128) - external_pk_name = models.CharField(blank=False, null=False, max_length=128) - external_pk_value = models.CharField(blank=False, null=False, max_length=128) + external_id_field = models.CharField(blank=False, null=False, max_length=128) + id_value = models.CharField(blank=False, null=False, max_length=128) external_link_href = models.CharField(blank=True, null=True, max_length=255) external_link_display_name = models.CharField(blank=True, null=True, max_length=255) timestamp = models.DateTimeField(blank=False, null=False, auto_now_add=True) @@ -147,7 +147,7 @@ class SyncConfigError(Exception): self.full_message = full_message -StaticMapping = namedtuple('StaticMapping', ('pk', 'pretix_model', 'external_object_type', 'pretix_pk', 'external_pk', 'property_mapping')) +StaticMapping = namedtuple('StaticMapping', ('pk', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mapping')) class OutboundSyncProvider: @@ -167,17 +167,17 @@ 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): + """ + 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") + """ if not hasattr(cls, 'identifier'): raise TypeError('Call this method on a derived class that defines an "identifier" attribute.') OrderSyncQueue.objects.create( @@ -198,37 +198,37 @@ class OutboundSyncProvider: info = cls.get_external_link_info(event, external_link_href, external_link_display_name) return make_link(info, '{val}') - """ - Optionally override to configure a different retry backoff behavior - """ def next_retry_date(self, sq): + """ + Optionally override to configure a different retry backoff behavior + """ return now() + timedelta(hours=1) - """ - Optionally override this method to exclude certain orders from sync by returning False - """ def order_valid_for_sync(self, order): + """ + Optionally override this method to exclude certain orders from sync by returning False + """ return True - """ - Implementations must override this property to provide the data mappings as a list of objects. - - They can return instances of the StaticMapping namedtuple defined above, or create their own - class (e.g. a Django model). - - The returned objects must have at least the following properties: - - pk: unique identifier - - pretix_model: which pretix model to use as data source in this mapping. possible values are - the keys of sourcefields.AVAILABLE_MODELS - - external_object_type: destination object type in the target system. opaque string of maximum 128 characters. - - pretix_pk: which pretix data field should be used to identify the mapped object. any - DataFieldInfo.key returned by sourcefields.get_data_fields for the combination of - Event and pretix_model - - external_pk: destination identifier field in the target system - - property_mapping: mapping configuration as generated by PropertyMappingFormSet.to_property_mapping_json() - """ @property def mappings(self): + """ + Implementations must override this property to provide the data mappings as a list of objects. + + They can return instances of the StaticMapping namedtuple defined above, or create their own + class (e.g. a Django model). + + :return: The returned objects must have at least the following properties: + + - `pk`: Unique identifier + - `pretix_model`: Which pretix model to use as data source in this mapping. Possible values are + the keys of ``sourcefields.AVAILABLE_MODELS`` + - `external_object_type`: Destination object type in the target system. opaque string of maximum 128 characters. + - `pretix_id_field`: Which pretix data field should be used to identify the mapped object. Any ``DataFieldInfo.key`` + returned by ``sourcefields.get_data_fields()`` for the combination of ``Event`` and ``pretix_model``. + - `external_id_field`: Destination identifier field in the target system. + - `property_mapping`: Mapping configuration as generated by ``PropertyMappingFormSet.to_property_mapping_json()``. + """ raise NotImplementedError def sync_queued_orders(self, queued_orders): @@ -304,46 +304,52 @@ 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. - - :param pk_field: Identifier field in the target system as provided in mapping.external_pk - :param pk_value: Identifier contents as retrieved from the property specified by mapping.pretix_pk of the model - specified by mapping.pretix_model - :param properties: All properties defined in mapping.property_mapping, as list of three-tuples - (external_field, value, overwrite) - :param inputs: All pretix model instances from which data can be retrieved for this mapping - :param mapping: The mapping object as returned by self.mappings - :param mapped_objects: Information about objects that were synced in the same sync run, by mapping definitions - *before* the current one in order of self.mappings. - Type is a dictionary {mapping.pk: [list of return values of this method]} - Useful to create associations between objects in the target system. - :return: { - "object_type": mapping.external_object_type, - "pk_field": pk_field, - "pk_value": pk_value, - "external_link_href": "https://external-system.example.com/backend/link/to/contact/123/", - "external_link_display_name": "Contact #123 - Jane Doe", - "...optionally further values you need in mapped_objects for association": 123456789, - } - - 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, + external_id_field, + id_value, properties: list, inputs: dict, mapping, mapped_objects: dict, **kwargs, ): + """ + 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. + + :param external_id_field: Identifier field in the external system as provided in mapping.external_identifier + :param id_value: Identifier contents as retrieved from the property specified by mapping.pretix_identifier of the model + specified by mapping.pretix_model + :param properties: All properties defined in mapping.property_mapping, as list of three-tuples + (external_field, value, overwrite) + :param inputs: All pretix model instances from which data can be retrieved for this mapping + :param mapping: The mapping object as returned by self.mappings + :param mapped_objects: Information about objects that were synced in the same sync run, by mapping definitions + *before* the current one in order of self.mappings. + Type is a dictionary {mapping.pk: [list of return values of this method]} + Useful to create associations between objects in the target system. + + Example code to create return value:: + + return { + # required: + "object_type": mapping.external_object_type, + "external_id_field": external_id_field, + "id_value": id_value, + + # optional: + "external_link_href": "https://external-system.example.com/backend/link/to/contact/123/", + "external_link_display_name": "Contact #123 - Jane Doe", + "...optionally further values you need in mapped_objects for association": 123456789, + } + + 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. + """ raise NotImplementedError() def sync_object( @@ -356,13 +362,13 @@ class OutboundSyncProvider: properties = self.get_properties(inputs, mapping.property_mapping) logger.debug("Properties: %r", properties) - pk_value = self.get_field_value(inputs, {"pretix_field": mapping.pretix_pk}) - if not pk_value: + id_value = self.get_field_value(inputs, {"pretix_field": mapping.pretix_id_field}) + if not id_value: return None info = self.sync_object_with_properties( - pk_field=mapping.external_pk, - pk_value=pk_value, + external_id_field=mapping.external_id_field, + id_value=id_value, properties=properties, inputs=inputs, mapping=mapping, @@ -371,8 +377,8 @@ class OutboundSyncProvider: OrderSyncLink.objects.create( order=inputs.get(ORDER), order_position=inputs.get(ORDER_POSITION), sync_provider=self.identifier, external_object_type=info.get('object_type'), - external_pk_name=info.get('pk_field'), - external_pk_value=info.get('pk_value'), + external_id_field=info.get('external_id_field'), + id_value=info.get('id_value'), external_link_href=info.get('external_link_href'), external_link_display_name=info.get('external_link_display_name'), ) @@ -411,15 +417,15 @@ class OutboundSyncProvider: self.finalize_sync_order(order) return mapped_objects - """ - Called after sync_object has been called successfully for all objects of a specific order. Can be used for saving - bulk information per order. - """ def finalize_sync_order(self, order): + """ + Called after sync_object has been called successfully for all objects of a specific order. Can be used for saving + bulk information per order. + """ pass - """ - Called after all orders of an event have been synced. Can be used for clean-up tasks (closing a session etc). - """ def close(self): + """ + Called after all orders of an event have been synced. Can be used for clean-up tasks (closing a session etc). + """ pass diff --git a/src/pretix/base/datasync/utils.py b/src/pretix/base/datasync/utils.py index f050677fdd..1e84f7e3c7 100644 --- a/src/pretix/base/datasync/utils.py +++ b/src/pretix/base/datasync/utils.py @@ -33,27 +33,27 @@ def assign_properties( ): out = {} - for k, v, mode in new_values: + for field_name, v, mode in new_values: if mode == MODE_OVERWRITE: - out[k] = v + out[field_name] = v continue elif mode == MODE_SET_IF_NEW and not is_new: continue if not v: continue - current_value = old_values.get(k, out.get(k, "")) + current_value = old_values.get(field_name, out.get(field_name, "")) if mode in (MODE_SET_IF_EMPTY, MODE_SET_IF_NEW): if not current_value: - out[k] = v + out[field_name] = v elif mode == MODE_APPEND_LIST: - _add_to_list(out, k, current_value, v, list_sep) + _add_to_list(out, field_name, current_value, v, list_sep) else: raise SyncConfigError(["Invalid update mode " + mode]) return out -def _add_to_list(out, key, current_value, new_item, list_sep): +def _add_to_list(out, field_name, current_value, new_item, list_sep): new_item = str(new_item) if list_sep is not None: new_item = new_item.replace(list_sep, "") @@ -64,4 +64,4 @@ def _add_to_list(out, key, current_value, new_item, list_sep): new_list = current_value + [new_item] if list_sep is not None: new_list = list_sep.join(new_list) - out[key] = new_list + out[field_name] = new_list diff --git a/src/tests/base/test_datasync.py b/src/tests/base/test_datasync.py index 1732af720d..9b30ca6c61 100644 --- a/src/tests/base/test_datasync.py +++ b/src/tests/base/test_datasync.py @@ -214,7 +214,7 @@ class SimpleOrderSync(OutboundSyncProvider): StaticMapping( pk=1, pretix_model='Order', external_object_type='ticketorders', - pretix_pk='event_order_code', external_pk='ordernumber', + pretix_id_field='event_order_code', external_id_field='ordernumber', property_mapping=json.dumps([ { "pretix_field": "email", @@ -309,7 +309,7 @@ def test_simple_order_sync(event): StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', ( - 'pk', 'pretix_model', 'external_object_type', 'pretix_pk', 'external_pk', 'property_mapping', 'association_mapping' + 'pk', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mapping', 'association_mapping' )) AssociationMapping = namedtuple('AssociationMapping', ( 'via_mapping_pk' @@ -326,7 +326,7 @@ class OrderAndTicketAssociationSync(OutboundSyncProvider): StaticMappingWithAssociations( pk=1, pretix_model='OrderPosition', external_object_type='tickets', - pretix_pk='ticket_id', external_pk='ticketnumber', + pretix_id_field='ticket_id', external_id_field='ticketnumber', property_mapping=json.dumps([ { "pretix_field": "ticket_price", @@ -379,7 +379,7 @@ class OrderAndTicketAssociationSync(OutboundSyncProvider): StaticMappingWithAssociations( pk=2, pretix_model='Order', external_object_type='ticketorders', - pretix_pk='event_order_code', external_pk='ordernumber', + pretix_id_field='event_order_code', external_id_field='ordernumber', property_mapping=json.dumps([ { "pretix_field": "email",