Add user interface for manual retry

This commit is contained in:
Mira Weller
2025-06-27 14:05:05 +02:00
parent ec64c0fc1b
commit c69c9d0119
6 changed files with 108 additions and 5 deletions

View File

@@ -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:

View File

@@ -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'),
},
]
})

View File

@@ -14,7 +14,7 @@
<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 pull-right">
{% csrf_token %}
{% if pending %}
{% if pending.not_before > now %}
{% if pending.not_before > now or pending.need_manual_retry %}
<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 %}
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
@@ -26,7 +26,10 @@
<p><b>{{ display_name }}</b></p>
{% if pending %}
<p>
{% if pending.failed_attempts %}
{% if pending.need_manual_retry %}
<i class="fa fa-warning"></i>
{% trans "Error" %}: {{ pending.get_need_manual_retry_display }}
{% elif pending.failed_attempts %}
<i class="fa fa-warning"></i>
{% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %}
Error. Retry {{ num }} of {{ max }}.

View File

@@ -0,0 +1,48 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block content %}
<h2>{% trans "Sync problems" %}</h2>
<form method="post">
{% csrf_token %}
<table class="table table-hover">
<thead>
<tr>
<th>
{% if queue_items %}
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
<th>{% trans "Order" %}</th>
<th>{% trans "Sync provider" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Failure mode" %}</th>
</tr>
</thead>
<tbody>
{% for item in queue_items %}
<tr>
<td><input type="checkbox" name="idlist" value="{{ item.pk }}"></td>
<td><a href="{% url "control:event.order" event=item.order.event.slug organizer=item.order.event.organizer.slug code=item.order.code %}">{{ item.order.full_code }}</a></td>
<td>{{ item.sync_provider }}</td>
<td>{{ item.triggered }}</td>
<td>{{ item.get_need_manual_retry_display }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5" align="center">{% trans "No problems." %}</td>
</tr>
{% endfor %}
</tbody>
{% if queue_items %}
<tfoot>
<tr><td colspan="5">
<button type="submit" name="action" value="retry" class="btn btn-primary"><i class="fa fa-refresh"></i> Retry selected</button>
<button type="submit" name="action" value="cancel" class="btn btn-danger"><i class="fa fa-times"></i> Cancel selected</button>
</td></tr>
</tfoot>
{% endif %}
</table>
</form>
{% endblock %}

View File

@@ -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<organizer>[^/]+)/export/(?P<pk>[^/]+)/delete$', organizer.DeleteScheduledExportView.as_view(),
name='organizer.export.scheduled.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ticket_select2$', typeahead.ticket_select2, name='organizer.ticket_select2'),
re_path(r'^organizer/(?P<organizer>[^/]+)/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'),

View File

@@ -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
)