diff --git a/src/pretix/base/datasync/datasync.py b/src/pretix/base/datasync/datasync.py index 873c5a4e06..34c2e96604 100644 --- a/src/pretix/base/datasync/datasync.py +++ b/src/pretix/base/datasync/datasync.py @@ -15,8 +15,8 @@ from django_scopes import scope, scopes_disabled from pretix.base.datasync.sourcefields import ( EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields, ) -from pretix.base.models import Event, Order -from pretix.base.services.tasks import TransactionAwareTask +from pretix.base.logentrytype_registry import make_link +from pretix.base.models import Order, OrderPosition from pretix.base.signals import EventPluginRegistry, periodic_task from pretix.celery_app import app @@ -58,6 +58,34 @@ class OrderSyncQueue(models.Model): return self.provider_class.max_attempts +class OrderSyncLink(models.Model): + class Meta: + indexes = [ + models.Index(fields=("order", "sync_provider")), + ] + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name="synced_objects" + ) + sync_provider = models.CharField(blank=False, null=False, max_length=128) + order_position = models.ForeignKey( + 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_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) + + def external_link_html(self): + if not self.external_link_display_name: + return None + + prov, meta = sync_targets.get(identifier=self.sync_provider) + if prov: + return prov.get_external_link_html(self.order.event, self.external_link_href, self.external_link_display_name) + + @receiver(periodic_task, dispatch_uid="data_sync_periodic") def on_periodic_task(sender, **kwargs): sync_all.apply_async() @@ -73,7 +101,6 @@ def sync_event_to_target(event, target_cls, queued_orders): p.sync_queued_orders(queued_orders) - @app.task() def sync_all(): with scopes_disabled(): @@ -100,7 +127,6 @@ StaticMapping = namedtuple('StaticMapping', ('pk', 'pretix_model', 'external_obj class OutboundSyncProvider: - #identifier = None max_attempts = 5 syncer_class = None @@ -126,8 +152,20 @@ class OutboundSyncProvider: triggered_by=triggered_by, not_before=not_before) + @classmethod + def get_external_link_info(cls, event, external_link_href, external_link_display_name): + return { + "href": external_link_href, + "val": external_link_display_name, + } + + @classmethod + def get_external_link_html(cls, event, external_link_href, external_link_display_name): + info = cls.get_external_link_info(event, external_link_href, external_link_display_name) + return make_link(info, '{val}') + def next_retry_date(self, sq): - return datetime.now() + timedelta(days=1) + return datetime.now() + timedelta(hours=1) def sync_queued_orders(self, queued_orders): for sq in queued_orders: @@ -135,16 +173,14 @@ class OutboundSyncProvider: mapped_objects = self.sync_order(sq.order) except SyncConfigError as e: logger.warning( - f"Could not sync order {sq.order.code} to {self.__name__} (config error)", + f"Could not sync order {sq.order.code} to {type(self).__name__} (config error)", exc_info=True, ) - sq.order.log_action( - "pretix.event.order.data_sync.failed", - { - "error": e.messages, - "full_message": e.full_message, - }, - ) + sq.order.log_action("pretix.event.order.data_sync.failed", { + "provider": self.identifier, + "error": e.messages, + "full_message": e.full_message, + }) sq.delete() except Exception as e: # TODO: different handling per Exception, or even per HTTP response code? @@ -156,20 +192,19 @@ class OutboundSyncProvider: ) if sq.failed_attempts >= self.max_attempts: sentry_sdk.capture_exception(e) - sq.order.log_action( - "pretix.event.order.data_sync.failed", - { - "error": [_("Maximum number of retries exceeded.")], - "full_message": str(e), - }, - ) + sq.order.log_action("pretix.event.order.data_sync.failed", { + "provider": self.identifier, + "error": [_("Maximum number of retries exceeded.")], + "full_message": str(e), + }) sq.delete() else: sq.save() else: - sq.order.log_action( - "pretix.event.order.data_sync.success", {"objects": mapped_objects} - ) + sq.order.log_action("pretix.event.order.data_sync.success", { + "provider": self.identifier, + "objects": mapped_objects + }) sq.delete() def order_valid_for_sync(self, order): @@ -177,7 +212,7 @@ class OutboundSyncProvider: @property def mappings(self): - raise NotImplemented + raise NotImplementedError @cached_property def data_fields(self): @@ -223,7 +258,16 @@ class OutboundSyncProvider: if not pk_value: return None - return self.sync_object_with_properties(inputs, mapping, mapped_objects, pk_value, properties) + info = self.sync_object_with_properties(inputs, mapping, mapped_objects, pk_value, properties) + 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_link_href=info.get('external_link_href'), + external_link_display_name=info.get('external_link_display_name'), + ) + return info def sync_order(self, order): if not self.order_valid_for_sync(order): @@ -238,6 +282,7 @@ class OutboundSyncProvider: "voucher", ) ) + order.synced_objects.filter(sync_provider=self.identifier).delete() order_inputs = {ORDER: order, EVENT: self.event} mapped_objects = {} for mapping in self.mappings: @@ -259,7 +304,7 @@ class OutboundSyncProvider: """ Called after sync_object has been called successfully for all objects of a specific order. Can be used for saving - bulk information per order. + bulk information per order. """ def finalize_sync_order(self, order): pass diff --git a/src/pretix/base/datasync/sourcefields.py b/src/pretix/base/datasync/sourcefields.py index 4d8ad64910..d01031daee 100644 --- a/src/pretix/base/datasync/sourcefields.py +++ b/src/pretix/base/datasync/sourcefields.py @@ -426,7 +426,7 @@ def translate_property_mappings(property_mapping, checkin_list_map): for mapping in mappings: if mapping["pretix_field"].startswith("checkin_date_"): - old_id = int(mapping["pretix_field"][len("checkin_date_") :]) + old_id = int(mapping["pretix_field"][len("checkin_date_"):]) mapping["pretix_field"] = "checkin_date_%d" % checkin_list_map[old_id].pk return json.dumps(mappings) @@ -437,6 +437,7 @@ def get_enum_opts(q): else: return None + QUESTION_TYPE_IDENTIFIERS = { Question.TYPE_NUMBER: "NUMBER", Question.TYPE_STRING: "STRING", diff --git a/src/pretix/control/datasync.py b/src/pretix/control/datasync.py index 30f11cf0b2..d5800458eb 100644 --- a/src/pretix/control/datasync.py +++ b/src/pretix/control/datasync.py @@ -1,3 +1,5 @@ +from itertools import groupby + from django.contrib import messages from django.dispatch import receiver from django.http import HttpResponseNotAllowed @@ -14,24 +16,23 @@ from pretix.control.views.orders import OrderView @receiver(order_info, dispatch_uid="datasync_control_order_info") def on_control_order_info(sender: Event, request, order: Order, **kwargs): providers = [provider for provider, meta in sync_targets.filter(active_in=sender)] - if not providers: return "" + if not providers: + return "" - queued = order.queued_sync_jobs.all() - queued_provider_ids = {p.sync_provider for p in queued} - non_pending = [(provider.identifier, provider.display_name) for provider in providers if provider.identifier not in queued_provider_ids] - - #sync_logs = order.all_logentries().filter(action_type__in=( - # "pretix.event.order.data_sync.success", - # "pretix.event.order.data_sync.failed" - #)) + queued = {p.sync_provider: p for p in order.queued_sync_jobs.all()} + objects = { + provider: list(objects) + for (provider, objects) + in groupby(order.synced_objects.order_by('sync_provider').all(), key=lambda o: o.sync_provider) + } + providers = [(provider.identifier, provider.display_name, queued.get(provider.identifier), objects.get(provider.identifier)) for provider in providers] template = get_template("pretixcontrol/datasync/control_order_info.html") ctx = { "order": order, "request": request, "event": sender, - "non_pending_providers": non_pending, - "queued_sync_jobs": queued, + "providers": providers, } return template.render(ctx, request=request) diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 2acadbbc7d..a7dd8aaa88 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -46,6 +46,7 @@ from django.utils.html import escape, format_html from django.utils.translation import gettext_lazy as _, pgettext_lazy from i18nfield.strings import LazyI18nString +from pretix.base.datasync.datasync import sync_targets from pretix.base.logentrytypes import ( DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType, ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType, @@ -420,11 +421,24 @@ class OrderPrintLogEntryType(OrderLogEntryType): type=dict(PrintLog.PRINT_TYPES)[data["type"]], ) + @log_entry_types.new_from_dict({ "pretix.event.order.data_sync.success": _("Ticket data successfully transferred to {provider}."), }) class OrderDataSyncLogentrytype(OrderLogEntryType): - pass + def display(self, logentry, data): + links = [] + if data.get('provider') and data.get('objects'): + prov, meta = sync_targets.get(identifier=data['provider']) + if prov: + for objs in data['objects'].values(): + links.append(", ".join( + prov.get_external_link_html(logentry.event, obj['external_link_href'], obj['external_link_display_name']) + for obj in objs + if obj and 'external_link_href' in obj and 'external_link_display_name' in obj + )) + + return mark_safe(super().display(logentry, data) + "".join("
" + link + "
" for link in links)) @log_entry_types.new_from_dict({ diff --git a/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html b/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html index 8abb2376b2..3af94f401e 100644 --- a/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html +++ b/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html @@ -12,10 +12,11 @@ {{ test.hello }}| {{ pending.sync_provider }} | +{{ display_name }} | + {% if pending %} {% if pending.failed_attempts %} {% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %} @@ -32,32 +33,40 @@ {% else %} {% trans "Pending" %} {% endif %} + (triggered by {{ pending.triggered_by }} at {{ pending.triggered }}) + {% else %} + - + {% endif %} | -- - | -
| {{ display_name }} | -- | -+ | |
| + | + {% if obj.external_link_html %} + {{ obj.external_link_html }} + {% else %} + {{ obj.external_object_type }} with {{ obj.external_pk_name }} = {{ obj.external_pk_value }} + {% endif %} + | +{{ obj.timestamp }} | +