diff --git a/src/pretix/base/models/datasync.py b/src/pretix/base/models/datasync.py
index 43d0429d35..0c8450ac5f 100644
--- a/src/pretix/base/models/datasync.py
+++ b/src/pretix/base/models/datasync.py
@@ -52,9 +52,9 @@ class OrderSyncQueue(models.Model):
failed_attempts = models.PositiveIntegerField(default=0)
not_before = models.DateTimeField(blank=False, null=False, db_index=True)
need_manual_retry = models.CharField(blank=True, null=True, choices=[
- ('recoverable', _('Temporary error, retry exceeded')),
- ('unrecoverable', _('Misconfiguration')),
- ('unhandled', _('Unhandled exception'))
+ ('recoverable', _('Temporary error, auto-retry limit exceeded')),
+ ('unrecoverable', _('Misconfiguration, please check provider settings')),
+ ('unhandled', _('System error, needs manual intervention'))
])
class Meta:
diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py
index bfd5aef7c0..5125c9965d 100644
--- a/src/pretix/control/navigation.py
+++ b/src/pretix/control/navigation.py
@@ -451,6 +451,11 @@ def get_global_navigation(request):
'url': reverse('control:global.sysreport'),
'active': (url.url_name == 'global.sysreport'),
},
+ {
+ 'label': _('Data sync problems'),
+ 'url': reverse('control:global.datasync.failedjobs'),
+ 'active': (url.url_name == 'global.datasync.failedjobs'),
+ },
]
})
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 04fb3ba03c..8a661ebaa9 100644
--- a/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html
+++ b/src/pretix/control/templates/pretixcontrol/datasync/control_order_info.html
@@ -14,7 +14,7 @@
+{% endblock %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index d738c0d279..de0477f122 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -58,6 +58,7 @@ urlpatterns = [
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
re_path(r'^global/sysreport/$', global_settings.SysReportView.as_view(), name='global.sysreport'),
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
+ re_path(r'^global/datasync/failedjobs/$', datasync.GlobalFailedSyncJobsView.as_view(), name='global.datasync.failedjobs'),
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
re_path(r'^logdetail/refund/$', global_settings.RefundDetailView.as_view(), name='global.refunddetail'),
@@ -248,6 +249,7 @@ urlpatterns = [
re_path(r'^organizer/(?P[^/]+)/export/(?P[^/]+)/delete$', organizer.DeleteScheduledExportView.as_view(),
name='organizer.export.scheduled.delete'),
re_path(r'^organizer/(?P[^/]+)/ticket_select2$', typeahead.ticket_select2, name='organizer.ticket_select2'),
+ re_path(r'^organizer/(?P[^/]+)/datasync/failedjobs/$', datasync.OrganizerFailedSyncJobsView.as_view(), name='organizer.datasync.failedjobs'),
re_path(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
re_path(r'^events/$', main.EventList.as_view(), name='events'),
re_path(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
diff --git a/src/pretix/control/views/datasync.py b/src/pretix/control/views/datasync.py
index 3c6436685a..00c49d2c82 100644
--- a/src/pretix/control/views/datasync.py
+++ b/src/pretix/control/views/datasync.py
@@ -29,9 +29,12 @@ from django.shortcuts import redirect
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
+from django.views.generic import TemplateView, ListView
from pretix.base.datasync.datasync import sync_targets
from pretix.base.models import Event, Order
+from pretix.base.models.datasync import OrderSyncQueue
+from pretix.control.permissions import AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin
from pretix.control.signals import order_info
from pretix.control.views.orders import OrderView
@@ -77,6 +80,7 @@ class ControlSyncJob(OrderView):
elif self.request.POST.get("run_job_now"):
job = self.order.queued_sync_jobs.get(pk=self.request.POST.get("run_job_now"))
job.not_before = now()
+ job.need_manual_retry = None
job.save()
messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
@@ -84,3 +88,44 @@ class ControlSyncJob(OrderView):
def get(self, *args, **kwargs):
return HttpResponseNotAllowed(['POST'])
+
+
+class FailedSyncJobsView(ListView):
+ template_name = 'pretixcontrol/datasync/failed_jobs.html'
+ model = OrderSyncQueue
+ context_object_name = 'queue_items'
+ paginate_by = 20
+ ordering = ('triggered',)
+
+ def get_queryset(self):
+ return super().get_queryset().filter(
+ need_manual_retry__isnull=False,
+ ).select_related(
+ 'order'
+ )
+
+ def post(self, request, *args, **kwargs):
+ items = self.get_queryset().filter(pk__in=request.POST.getlist('idlist'))
+
+ if self.request.POST.get("action") == "retry":
+ for item in items:
+ item.not_before = now()
+ item.need_manual_retry = None
+ item.save()
+ messages.success(self.request, _('The selected jobs have been set to run as soon as possible.'))
+ elif self.request.POST.get("action") == "cancel":
+ items.delete()
+ messages.success(self.request, _('The selected jobs have been canceled.'))
+
+ return redirect(request.get_full_path())
+
+
+class GlobalFailedSyncJobsView(AdministratorPermissionRequiredMixin, FailedSyncJobsView):
+ pass
+
+
+class OrganizerFailedSyncJobsView(OrganizerPermissionRequiredMixin, FailedSyncJobsView):
+ def get_queryset(self):
+ return super().get_queryset().filter(
+ event__organizer=self.request.organizer
+ )