From c69c9d01196fbfe184ea8134d503e2659beeff23 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Fri, 27 Jun 2025 14:05:05 +0200 Subject: [PATCH] Add user interface for manual retry --- src/pretix/base/models/datasync.py | 6 +-- src/pretix/control/navigation.py | 5 ++ .../datasync/control_order_info.html | 7 ++- .../pretixcontrol/datasync/failed_jobs.html | 48 +++++++++++++++++++ src/pretix/control/urls.py | 2 + src/pretix/control/views/datasync.py | 45 +++++++++++++++++ 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/datasync/failed_jobs.html 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 @@
{% csrf_token %} {% if pending %} - {% if pending.not_before > now %} + {% if pending.not_before > now or pending.need_manual_retry %} {% endif %} @@ -26,7 +26,10 @@

{{ display_name }}

{% if pending %}

- {% if pending.failed_attempts %} + {% if pending.need_manual_retry %} + + {% trans "Error" %}: {{ pending.get_need_manual_retry_display }} + {% elif pending.failed_attempts %} {% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %} Error. Retry {{ num }} of {{ max }}. diff --git a/src/pretix/control/templates/pretixcontrol/datasync/failed_jobs.html b/src/pretix/control/templates/pretixcontrol/datasync/failed_jobs.html new file mode 100644 index 0000000000..756780deed --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/datasync/failed_jobs.html @@ -0,0 +1,48 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} + +{% block content %} +

{% trans "Sync problems" %}

+ + {% csrf_token %} + + + + + + + + + + + + {% for item in queue_items %} + + + + + + + + {% empty %} + + + + {% endfor %} + + {% if queue_items %} + + + + {% endif %} +
+ {% if queue_items %} + + {% endif %} + {% trans "Order" %}{% trans "Sync provider" %}{% trans "Date" %}{% trans "Failure mode" %}
{{ item.order.full_code }}{{ item.sync_provider }}{{ item.triggered }}{{ item.get_need_manual_retry_display }}
{% trans "No problems." %}
+ + +
+
+{% 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 + )