mirror of
https://github.com/pretix/pretix.git
synced 2026-05-14 16:44:06 +00:00
516 lines
18 KiB
Python
516 lines
18 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
import json
|
|
from collections import defaultdict, namedtuple
|
|
from datetime import timedelta
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
from django.utils.timezone import now
|
|
from django_scopes import scope
|
|
|
|
from pretix.base.datasync.datasync import (
|
|
MODE_APPEND_LIST, MODE_OVERWRITE, MODE_SET_IF_EMPTY, MODE_SET_IF_NEW,
|
|
OutboundSyncProvider, StaticMapping, sync_all, sync_targets,
|
|
)
|
|
from pretix.base.datasync.utils import assign_properties
|
|
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def event():
|
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
|
event = Event.objects.create(
|
|
organizer=o, name='Dummy', slug='dummy',
|
|
date_from=now(),
|
|
plugins='pretix.plugins.banktransfer,testplugin'
|
|
)
|
|
event.settings.name_scheme = 'given_family'
|
|
with scope(organizer=o):
|
|
o1 = Order.objects.create(
|
|
code='1AAA', event=event, email='anonymous@example.org',
|
|
status=Order.STATUS_PENDING, locale='en',
|
|
datetime=now(), expires=now() + timedelta(days=10),
|
|
total=46,
|
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
|
)
|
|
o2 = Order.objects.create(
|
|
code='2EEE', event=event, email='ephemeral@example.com',
|
|
status=Order.STATUS_PENDING, locale='en',
|
|
datetime=now(), expires=now() + timedelta(days=10),
|
|
total=23,
|
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
|
)
|
|
ticket = Item.objects.create(event=event, name='Early-bird ticket',
|
|
default_price=Decimal('23.00'), admission=True)
|
|
OrderPosition.objects.create(
|
|
order=o1, item=ticket, variation=None,
|
|
price=Decimal("23.00"), attendee_name_parts={'given_name': "Alice", 'family_name': "Anonymous"}, positionid=1
|
|
)
|
|
OrderPosition.objects.create(
|
|
order=o1, item=ticket, variation=None,
|
|
price=Decimal("23.00"), attendee_name_parts={'given_name': "Charlie", 'family_name': "C."}, positionid=2
|
|
)
|
|
OrderPosition.objects.create(
|
|
order=o2, item=ticket, variation=None,
|
|
price=Decimal("23.00"), attendee_name_parts={'given_name': "Eve", 'family_name': "Ephemeral"}, positionid=1
|
|
)
|
|
yield event
|
|
|
|
|
|
def expected_order_sync_result():
|
|
return {
|
|
'ticketorders': [
|
|
{
|
|
'_id': 0,
|
|
'ordernumber': 'DUMMY-1AAA',
|
|
'orderemail': 'anonymous@example.org',
|
|
'status': 'pending',
|
|
'total': '46.00',
|
|
},
|
|
{
|
|
'_id': 1,
|
|
'ordernumber': 'DUMMY-2EEE',
|
|
'orderemail': 'ephemeral@example.com',
|
|
'status': 'pending',
|
|
'total': '23.00',
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def expected_sync_result_with_associations():
|
|
return {
|
|
'tickets': [
|
|
{
|
|
'_id': 0,
|
|
'ticketnumber': '1AAA-1',
|
|
'amount': '23.00',
|
|
'firstname': 'Alice',
|
|
'lastname': 'Anonymous',
|
|
'status': 'pending',
|
|
'links': [],
|
|
},
|
|
{
|
|
'_id': 1,
|
|
'ticketnumber': '1AAA-2',
|
|
'amount': '23.00',
|
|
'firstname': 'Charlie',
|
|
'lastname': 'C.',
|
|
'status': 'pending',
|
|
'links': [],
|
|
},
|
|
{
|
|
'_id': 2,
|
|
'ticketnumber': '2EEE-1',
|
|
'amount': '23.00',
|
|
'firstname': 'Eve',
|
|
'lastname': 'Ephemeral',
|
|
'status': 'pending',
|
|
'links': [],
|
|
},
|
|
],
|
|
'ticketorders': [
|
|
{
|
|
'_id': 0,
|
|
'ordernumber': 'DUMMY-1AAA',
|
|
'orderemail': 'anonymous@example.org',
|
|
'firstname': '',
|
|
'lastname': '',
|
|
'status': 'pending',
|
|
'links': ['link:tickets:0', 'link:tickets:1'],
|
|
},
|
|
{
|
|
'_id': 1,
|
|
'ordernumber': 'DUMMY-2EEE',
|
|
'orderemail': 'ephemeral@example.com',
|
|
'firstname': '',
|
|
'lastname': '',
|
|
'status': 'pending',
|
|
'links': ['link:tickets:2'],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _register_with_fake_plugin_name(registry, obj, plugin_name):
|
|
class App:
|
|
name = plugin_name
|
|
registry.register(obj)
|
|
registry.registered_entries[obj]['plugin'] = App
|
|
|
|
|
|
class FakeSyncAPI:
|
|
def __init__(self):
|
|
self.fake_database = defaultdict(list)
|
|
|
|
def retrieve_object(self, table, search_by_attribute, search_for_value):
|
|
t = self.fake_database[table]
|
|
for idx, record in enumerate(t):
|
|
if record.get(search_by_attribute) == search_for_value:
|
|
return {**record, "_id": idx}
|
|
return None
|
|
|
|
def create_or_update_object(self, table, record):
|
|
t = self.fake_database[table]
|
|
if record.get("_id") is not None:
|
|
t[record["_id"]].update(record)
|
|
else:
|
|
record["_id"] = len(t)
|
|
t.append(record)
|
|
return record
|
|
|
|
|
|
class SimpleOrderSync(OutboundSyncProvider):
|
|
identifier = "example1"
|
|
fake_api_client = None
|
|
|
|
@property
|
|
def mappings(self):
|
|
return [
|
|
StaticMapping(
|
|
pk=1,
|
|
pretix_model='Order', external_object_type='ticketorders',
|
|
pretix_pk='event_order_code', external_pk='ordernumber',
|
|
property_mapping=json.dumps([
|
|
{
|
|
"pretix_field": "email",
|
|
"external_field": "orderemail",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "order_status",
|
|
"external_field": "status",
|
|
"value_map": json.dumps({
|
|
Order.STATUS_PENDING: "pending",
|
|
Order.STATUS_PAID: "paid",
|
|
Order.STATUS_EXPIRED: "expired",
|
|
Order.STATUS_CANCELED: "canceled",
|
|
Order.STATUS_REFUNDED: "refunded",
|
|
}),
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "order_total",
|
|
"external_field": "total",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
])
|
|
)
|
|
]
|
|
|
|
def sync_object_with_properties(
|
|
self,
|
|
pk_field,
|
|
pk_value,
|
|
properties: list,
|
|
inputs: dict,
|
|
mapping,
|
|
mapped_objects: dict,
|
|
**kwargs,
|
|
):
|
|
pre_existing_object = self.fake_api_client.retrieve_object(mapping.external_object_type, pk_field, pk_value)
|
|
update_values = assign_properties(properties, pre_existing_object or {}, is_new=pre_existing_object is None)
|
|
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
|
|
**update_values,
|
|
pk_field: pk_value,
|
|
"_id": pre_existing_object and pre_existing_object.get("_id"),
|
|
})
|
|
|
|
return {
|
|
"object_type": mapping.external_object_type,
|
|
"pk_field": pk_field,
|
|
"pk_value": pk_value,
|
|
"external_link_href": f"https://external-system.example.com/backend/link/to/{mapping.external_object_type}/123/",
|
|
"external_link_display_name": "Contact #123 - Jane Doe",
|
|
"my_result": result,
|
|
}
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_simple_order_sync(event):
|
|
_register_with_fake_plugin_name(sync_targets, SimpleOrderSync, 'testplugin')
|
|
|
|
for order in event.orders.order_by("code").all():
|
|
SimpleOrderSync.enqueue_order(order, 'testcase')
|
|
|
|
SimpleOrderSync.fake_api_client = FakeSyncAPI()
|
|
|
|
sync_all()
|
|
|
|
expected = expected_order_sync_result()
|
|
assert SimpleOrderSync.fake_api_client.fake_database == expected
|
|
|
|
order_1a = event.orders.get(code='1AAA')
|
|
order_1a.status = Order.STATUS_PAID
|
|
order_1a.save()
|
|
|
|
for order in event.orders.order_by("code").all():
|
|
SimpleOrderSync.enqueue_order(order, 'testcase')
|
|
|
|
sync_all()
|
|
|
|
expected['ticketorders'][0]['status'] = 'paid'
|
|
assert SimpleOrderSync.fake_api_client.fake_database == expected
|
|
|
|
|
|
StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', (
|
|
'pk', 'pretix_model', 'external_object_type', 'pretix_pk', 'external_pk', 'property_mapping', 'association_mapping'
|
|
))
|
|
AssociationMapping = namedtuple('AssociationMapping', (
|
|
'via_mapping_pk'
|
|
))
|
|
|
|
|
|
class OrderAndTicketAssociationSync(OutboundSyncProvider):
|
|
identifier = "example2"
|
|
fake_api_client = None
|
|
|
|
@property
|
|
def mappings(self):
|
|
return [
|
|
StaticMappingWithAssociations(
|
|
pk=1,
|
|
pretix_model='OrderPosition', external_object_type='tickets',
|
|
pretix_pk='ticket_id', external_pk='ticketnumber',
|
|
property_mapping=json.dumps([
|
|
{
|
|
"pretix_field": "ticket_price",
|
|
"external_field": "amount",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "attendee_name_given_name",
|
|
"external_field": "firstname",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "attendee_name_family_name",
|
|
"external_field": "lastname",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "order_status",
|
|
"external_field": "status",
|
|
"value_map": json.dumps({
|
|
Order.STATUS_PENDING: "pending",
|
|
Order.STATUS_PAID: "paid",
|
|
Order.STATUS_EXPIRED: "expired",
|
|
Order.STATUS_CANCELED: "canceled",
|
|
Order.STATUS_REFUNDED: "refunded",
|
|
}),
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
]),
|
|
association_mapping=[],
|
|
),
|
|
StaticMappingWithAssociations(
|
|
pk=2,
|
|
pretix_model='Order', external_object_type='ticketorders',
|
|
pretix_pk='event_order_code', external_pk='ordernumber',
|
|
property_mapping=json.dumps([
|
|
{
|
|
"pretix_field": "email",
|
|
"external_field": "orderemail",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "invoice_address_name_given_name",
|
|
"external_field": "firstname",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "invoice_address_name_family_name",
|
|
"external_field": "lastname",
|
|
"value_map": "",
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
{
|
|
"pretix_field": "order_status",
|
|
"external_field": "status",
|
|
"value_map": json.dumps({
|
|
Order.STATUS_PENDING: "pending",
|
|
Order.STATUS_PAID: "paid",
|
|
Order.STATUS_EXPIRED: "expired",
|
|
Order.STATUS_CANCELED: "canceled",
|
|
Order.STATUS_REFUNDED: "refunded",
|
|
}),
|
|
"overwrite": MODE_OVERWRITE,
|
|
},
|
|
]),
|
|
association_mapping=[
|
|
AssociationMapping(via_mapping_pk=1)
|
|
],
|
|
),
|
|
]
|
|
|
|
def sync_object_with_properties(
|
|
self,
|
|
pk_field,
|
|
pk_value,
|
|
properties: list,
|
|
inputs: dict,
|
|
mapping,
|
|
mapped_objects: dict,
|
|
**kwargs,
|
|
):
|
|
pre_existing_object = self.fake_api_client.retrieve_object(mapping.external_object_type, pk_field, pk_value)
|
|
update_values = assign_properties(properties, pre_existing_object or {}, is_new=pre_existing_object is None)
|
|
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
|
|
**update_values,
|
|
pk_field: pk_value,
|
|
"_id": pre_existing_object and pre_existing_object.get("_id"),
|
|
"links": [f"link:{obj['object_type']}:{obj['my_result']['_id']}" for am in mapping.association_mapping for obj in mapped_objects[am.via_mapping_pk]]
|
|
})
|
|
|
|
return {
|
|
"object_type": mapping.external_object_type,
|
|
"pk_field": pk_field,
|
|
"pk_value": pk_value,
|
|
"external_link_href": f"https://external-system.example.com/backend/link/to/{mapping.external_object_type}/123/",
|
|
"external_link_display_name": "Contact #123 - Jane Doe",
|
|
"my_result": result,
|
|
}
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_association_sync(event):
|
|
_register_with_fake_plugin_name(sync_targets, OrderAndTicketAssociationSync, 'testplugin')
|
|
|
|
for order in event.orders.order_by("code").all():
|
|
OrderAndTicketAssociationSync.enqueue_order(order, 'testcase')
|
|
|
|
OrderAndTicketAssociationSync.fake_api_client = FakeSyncAPI()
|
|
|
|
sync_all()
|
|
|
|
expected = expected_sync_result_with_associations()
|
|
assert OrderAndTicketAssociationSync.fake_api_client.fake_database == expected
|
|
|
|
order_1a = event.orders.get(code='1AAA')
|
|
order_1a.status = Order.STATUS_PAID
|
|
order_1a.save()
|
|
|
|
for order in event.orders.order_by("code").all():
|
|
OrderAndTicketAssociationSync.enqueue_order(order, 'testcase')
|
|
|
|
sync_all()
|
|
|
|
expected['tickets'][0]['status'] = 'paid'
|
|
expected['tickets'][1]['status'] = 'paid'
|
|
expected['ticketorders'][0]['status'] = 'paid'
|
|
assert OrderAndTicketAssociationSync.fake_api_client.fake_database == expected
|
|
|
|
|
|
def test_assign_properties():
|
|
assert assign_properties(
|
|
[("name", "Alice", MODE_OVERWRITE)], {"name": "A"}, is_new=False
|
|
) == {"name": "Alice"}
|
|
assert (
|
|
assign_properties([("name", "Alice", MODE_SET_IF_NEW)], {}, is_new=False) == {}
|
|
)
|
|
assert assign_properties([("name", "Alice", MODE_SET_IF_NEW)], {}, is_new=True) == {
|
|
"name": "Alice"
|
|
}
|
|
assert assign_properties(
|
|
[
|
|
("name", "Alice", MODE_SET_IF_NEW),
|
|
("name", "A", MODE_SET_IF_NEW),
|
|
],
|
|
{},
|
|
is_new=True,
|
|
) == {"name": "Alice"}
|
|
assert (
|
|
assign_properties(
|
|
[
|
|
("name", "Alice", MODE_SET_IF_NEW),
|
|
("name", "A", MODE_SET_IF_NEW),
|
|
],
|
|
{"name": "Bob"},
|
|
is_new=False,
|
|
)
|
|
== {}
|
|
)
|
|
assert (
|
|
assign_properties(
|
|
[
|
|
("name", "Alice", MODE_SET_IF_NEW),
|
|
("name", "A", MODE_SET_IF_NEW),
|
|
],
|
|
{},
|
|
is_new=False,
|
|
)
|
|
== {}
|
|
)
|
|
assert assign_properties(
|
|
[
|
|
("name", "Alice", MODE_SET_IF_EMPTY),
|
|
("name", "A", MODE_SET_IF_EMPTY),
|
|
],
|
|
{},
|
|
is_new=True,
|
|
) == {"name": "Alice"}
|
|
assert (
|
|
assign_properties(
|
|
[
|
|
("name", "Alice", MODE_SET_IF_EMPTY),
|
|
("name", "A", MODE_SET_IF_EMPTY),
|
|
],
|
|
{"name": "Bob"},
|
|
is_new=False,
|
|
)
|
|
== {}
|
|
)
|
|
assert assign_properties(
|
|
[("name", "Alice", MODE_SET_IF_EMPTY)], {}, is_new=False
|
|
) == {"name": "Alice"}
|
|
|
|
assert assign_properties(
|
|
[("name", "Alice", MODE_SET_IF_EMPTY)], {}, is_new=False
|
|
) == {"name": "Alice"}
|
|
|
|
assert assign_properties(
|
|
[("colors", "red", MODE_APPEND_LIST)], {}, is_new=False
|
|
) == {"colors": "red"}
|
|
assert assign_properties(
|
|
[("colors", "red", MODE_APPEND_LIST)], {"colors": "red"}, is_new=False
|
|
) == {}
|
|
assert assign_properties(
|
|
[("colors", "red", MODE_APPEND_LIST)], {"colors": "blue"}, is_new=False
|
|
) == {"colors": "blue;red"}
|
|
assert assign_properties(
|
|
[("colors", "red", MODE_APPEND_LIST)], {"colors": "green;blue"}, is_new=False
|
|
) == {"colors": "green;blue;red"}
|
|
assert assign_properties(
|
|
[("colors", "red", MODE_APPEND_LIST)], {"colors": ["green", "blue"]}, is_new=False, list_sep=None
|
|
) == {"colors": ["green", "blue", "red"]}
|
|
assert assign_properties(
|
|
[("colors", "green", MODE_APPEND_LIST)], {"colors": ["green", "blue"]}, is_new=False, list_sep=None
|
|
) == {}
|