mirror of
https://github.com/pretix/pretix.git
synced 2026-05-20 17:44:02 +00:00
Store information about objects transferred in last successful sync
This commit is contained in:
@@ -15,8 +15,8 @@ from django_scopes import scope, scopes_disabled
|
|||||||
from pretix.base.datasync.sourcefields import (
|
from pretix.base.datasync.sourcefields import (
|
||||||
EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields,
|
EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Event, Order
|
from pretix.base.logentrytype_registry import make_link
|
||||||
from pretix.base.services.tasks import TransactionAwareTask
|
from pretix.base.models import Order, OrderPosition
|
||||||
from pretix.base.signals import EventPluginRegistry, periodic_task
|
from pretix.base.signals import EventPluginRegistry, periodic_task
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
@@ -58,6 +58,34 @@ class OrderSyncQueue(models.Model):
|
|||||||
return self.provider_class.max_attempts
|
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")
|
@receiver(periodic_task, dispatch_uid="data_sync_periodic")
|
||||||
def on_periodic_task(sender, **kwargs):
|
def on_periodic_task(sender, **kwargs):
|
||||||
sync_all.apply_async()
|
sync_all.apply_async()
|
||||||
@@ -73,7 +101,6 @@ def sync_event_to_target(event, target_cls, queued_orders):
|
|||||||
p.sync_queued_orders(queued_orders)
|
p.sync_queued_orders(queued_orders)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.task()
|
@app.task()
|
||||||
def sync_all():
|
def sync_all():
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
@@ -100,7 +127,6 @@ StaticMapping = namedtuple('StaticMapping', ('pk', 'pretix_model', 'external_obj
|
|||||||
|
|
||||||
|
|
||||||
class OutboundSyncProvider:
|
class OutboundSyncProvider:
|
||||||
#identifier = None
|
|
||||||
max_attempts = 5
|
max_attempts = 5
|
||||||
syncer_class = None
|
syncer_class = None
|
||||||
|
|
||||||
@@ -126,8 +152,20 @@ class OutboundSyncProvider:
|
|||||||
triggered_by=triggered_by,
|
triggered_by=triggered_by,
|
||||||
not_before=not_before)
|
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):
|
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):
|
def sync_queued_orders(self, queued_orders):
|
||||||
for sq in queued_orders:
|
for sq in queued_orders:
|
||||||
@@ -135,16 +173,14 @@ class OutboundSyncProvider:
|
|||||||
mapped_objects = self.sync_order(sq.order)
|
mapped_objects = self.sync_order(sq.order)
|
||||||
except SyncConfigError as e:
|
except SyncConfigError as e:
|
||||||
logger.warning(
|
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,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
sq.order.log_action(
|
sq.order.log_action("pretix.event.order.data_sync.failed", {
|
||||||
"pretix.event.order.data_sync.failed",
|
"provider": self.identifier,
|
||||||
{
|
|
||||||
"error": e.messages,
|
"error": e.messages,
|
||||||
"full_message": e.full_message,
|
"full_message": e.full_message,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
sq.delete()
|
sq.delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: different handling per Exception, or even per HTTP response code?
|
# TODO: different handling per Exception, or even per HTTP response code?
|
||||||
@@ -156,20 +192,19 @@ class OutboundSyncProvider:
|
|||||||
)
|
)
|
||||||
if sq.failed_attempts >= self.max_attempts:
|
if sq.failed_attempts >= self.max_attempts:
|
||||||
sentry_sdk.capture_exception(e)
|
sentry_sdk.capture_exception(e)
|
||||||
sq.order.log_action(
|
sq.order.log_action("pretix.event.order.data_sync.failed", {
|
||||||
"pretix.event.order.data_sync.failed",
|
"provider": self.identifier,
|
||||||
{
|
|
||||||
"error": [_("Maximum number of retries exceeded.")],
|
"error": [_("Maximum number of retries exceeded.")],
|
||||||
"full_message": str(e),
|
"full_message": str(e),
|
||||||
},
|
})
|
||||||
)
|
|
||||||
sq.delete()
|
sq.delete()
|
||||||
else:
|
else:
|
||||||
sq.save()
|
sq.save()
|
||||||
else:
|
else:
|
||||||
sq.order.log_action(
|
sq.order.log_action("pretix.event.order.data_sync.success", {
|
||||||
"pretix.event.order.data_sync.success", {"objects": mapped_objects}
|
"provider": self.identifier,
|
||||||
)
|
"objects": mapped_objects
|
||||||
|
})
|
||||||
sq.delete()
|
sq.delete()
|
||||||
|
|
||||||
def order_valid_for_sync(self, order):
|
def order_valid_for_sync(self, order):
|
||||||
@@ -177,7 +212,7 @@ class OutboundSyncProvider:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mappings(self):
|
def mappings(self):
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def data_fields(self):
|
def data_fields(self):
|
||||||
@@ -223,7 +258,16 @@ class OutboundSyncProvider:
|
|||||||
if not pk_value:
|
if not pk_value:
|
||||||
return None
|
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):
|
def sync_order(self, order):
|
||||||
if not self.order_valid_for_sync(order):
|
if not self.order_valid_for_sync(order):
|
||||||
@@ -238,6 +282,7 @@ class OutboundSyncProvider:
|
|||||||
"voucher",
|
"voucher",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
order.synced_objects.filter(sync_provider=self.identifier).delete()
|
||||||
order_inputs = {ORDER: order, EVENT: self.event}
|
order_inputs = {ORDER: order, EVENT: self.event}
|
||||||
mapped_objects = {}
|
mapped_objects = {}
|
||||||
for mapping in self.mappings:
|
for mapping in self.mappings:
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ def get_enum_opts(q):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
QUESTION_TYPE_IDENTIFIERS = {
|
QUESTION_TYPE_IDENTIFIERS = {
|
||||||
Question.TYPE_NUMBER: "NUMBER",
|
Question.TYPE_NUMBER: "NUMBER",
|
||||||
Question.TYPE_STRING: "STRING",
|
Question.TYPE_STRING: "STRING",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from itertools import groupby
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpResponseNotAllowed
|
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")
|
@receiver(order_info, dispatch_uid="datasync_control_order_info")
|
||||||
def on_control_order_info(sender: Event, request, order: Order, **kwargs):
|
def on_control_order_info(sender: Event, request, order: Order, **kwargs):
|
||||||
providers = [provider for provider, meta in sync_targets.filter(active_in=sender)]
|
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 = {p.sync_provider: p for p in order.queued_sync_jobs.all()}
|
||||||
queued_provider_ids = {p.sync_provider for p in queued}
|
objects = {
|
||||||
non_pending = [(provider.identifier, provider.display_name) for provider in providers if provider.identifier not in queued_provider_ids]
|
provider: list(objects)
|
||||||
|
for (provider, objects)
|
||||||
#sync_logs = order.all_logentries().filter(action_type__in=(
|
in groupby(order.synced_objects.order_by('sync_provider').all(), key=lambda o: o.sync_provider)
|
||||||
# "pretix.event.order.data_sync.success",
|
}
|
||||||
# "pretix.event.order.data_sync.failed"
|
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")
|
template = get_template("pretixcontrol/datasync/control_order_info.html")
|
||||||
ctx = {
|
ctx = {
|
||||||
"order": order,
|
"order": order,
|
||||||
"request": request,
|
"request": request,
|
||||||
"event": sender,
|
"event": sender,
|
||||||
"non_pending_providers": non_pending,
|
"providers": providers,
|
||||||
"queued_sync_jobs": queued,
|
|
||||||
}
|
}
|
||||||
return template.render(ctx, request=request)
|
return template.render(ctx, request=request)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from django.utils.html import escape, format_html
|
|||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.base.datasync.datasync import sync_targets
|
||||||
from pretix.base.logentrytypes import (
|
from pretix.base.logentrytypes import (
|
||||||
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
|
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
|
||||||
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
|
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
|
||||||
@@ -420,11 +421,24 @@ class OrderPrintLogEntryType(OrderLogEntryType):
|
|||||||
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
|
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@log_entry_types.new_from_dict({
|
@log_entry_types.new_from_dict({
|
||||||
"pretix.event.order.data_sync.success": _("Ticket data successfully transferred to {provider}."),
|
"pretix.event.order.data_sync.success": _("Ticket data successfully transferred to {provider}."),
|
||||||
})
|
})
|
||||||
class OrderDataSyncLogentrytype(OrderLogEntryType):
|
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("<p>" + link + "</p>" for link in links))
|
||||||
|
|
||||||
|
|
||||||
@log_entry_types.new_from_dict({
|
@log_entry_types.new_from_dict({
|
||||||
|
|||||||
@@ -12,10 +12,11 @@
|
|||||||
{{ test.hello }}
|
{{ test.hello }}
|
||||||
<table class="table table-condensed">
|
<table class="table table-condensed">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for pending in queued_sync_jobs %}
|
{% for identifier, display_name, pending, objects in providers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pending.sync_provider }}</td>
|
<td>{{ display_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if pending %}
|
||||||
{% if pending.failed_attempts %}
|
{% if pending.failed_attempts %}
|
||||||
<i class="fa fa-warning"></i>
|
<i class="fa fa-warning"></i>
|
||||||
{% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %}
|
{% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %}
|
||||||
@@ -32,32 +33,40 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<i class="fa fa-hourglass"></i> {% trans "Pending" %}
|
<i class="fa fa-hourglass"></i> {% trans "Pending" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
(triggered by {{ pending.triggered_by }} at {{ pending.triggered }})
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td align="right">
|
||||||
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=pending.sync_provider %}" method="post" class="form-inline">
|
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% if pending %}
|
||||||
{% if pending.not_before %}
|
{% if pending.not_before %}
|
||||||
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
|
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-trash"></i> {% trans "Cancel" %}</button>
|
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-trash"></i> {% trans "Cancel" %}</button>
|
||||||
</form>
|
{% else %}
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% for identifier, display_name in non_pending_providers %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ display_name }}</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>
|
|
||||||
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
|
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
|
||||||
<input type="hidden" name="queue_sync" value="true">
|
<input type="hidden" name="queue_sync" value="true">
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% for obj in objects %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
{% if obj.external_link_html %}
|
||||||
|
{{ obj.external_link_html }}
|
||||||
|
{% else %}
|
||||||
|
{{ obj.external_object_type }} with {{ obj.external_pk_name }} = {{ obj.external_pk_value }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td align="right">{{ obj.timestamp }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1677,6 +1677,3 @@ class EventQRCode(EventPermissionRequiredMixin, View):
|
|||||||
r = HttpResponse(byte_io.read(), content_type='image/' + filetype)
|
r = HttpResponse(byte_io.read(), content_type='image/' + filetype)
|
||||||
r['Content-Disposition'] = f'inline; filename="qrcode-{request.event.slug}.{filetype}"'
|
r['Content-Disposition'] = f'inline; filename="qrcode-{request.event.slug}.{filetype}"'
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
#class DataSyncSettings
|
|
||||||
|
|||||||
Reference in New Issue
Block a user