Add control interface for pending data syncs

This commit is contained in:
Mira Weller
2025-02-28 15:01:53 +01:00
parent 79ea74ac6d
commit 6a0d316b82
12 changed files with 497 additions and 64 deletions

View File

@@ -42,3 +42,4 @@ class PretixControlConfig(AppConfig):
def ready(self):
from .views import dashboards # noqa
from . import logdisplay # noqa
from . import datasync # noqa

View File

@@ -0,0 +1,60 @@
from django.contrib import messages
from django.dispatch import receiver
from django.http import HttpResponseNotAllowed
from django.shortcuts import redirect
from django.template.loader import get_template
from pretix.base.datasync.datasync import sync_targets
from pretix.base.models import Event, Order
from pretix.control.signals import order_info
from pretix.control.views.orders import OrderView
from django.utils.translation import gettext_lazy as _
@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 ""
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"
#))
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,
}
return template.render(ctx, request=request)
class ControlSyncJob(OrderView):
permission = 'can_change_orders'
def post(self, request, provider, *args, **kwargs):
prov, meta = sync_targets.get(active_in=self.request.event, identifier=provider)
if self.request.POST.get("queue_sync") == "true":
prov.enqueue_order(self.order, 'user')
messages.success(self.request, _('The sync job has been enqueued and will run in the next minutes.'))
elif self.request.POST.get("cancel_job"):
job = self.order.queued_sync_jobs.get(pk=self.request.POST.get("cancel_job"))
job.delete()
messages.success(self.request, _('The sync job has been canceled.'))
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 = None
job.save()
messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs):
return HttpResponseNotAllowed(['POST'])

View File

@@ -0,0 +1,72 @@
from django import forms
from django.forms import formset_factory
from django.utils.translation import gettext_lazy as _
from pretix.base.datasync.sourcefields import QUESTION_TYPE_IDENTIFIERS
from pretix.base.datasync.datasync import MODE_SET_IF_NEW, MODE_SET_IF_EMPTY, MODE_OVERWRITE, MODE_APPEND_LIST
class PropertyMappingForm(forms.Form):
pretix_field = forms.CharField()
external_field = forms.CharField()
value_map = forms.CharField(required=False)
overwrite = forms.ChoiceField(
choices=[
(MODE_OVERWRITE, _("Overwrite")),
(MODE_SET_IF_NEW, _("Fill if new contact")),
(MODE_SET_IF_EMPTY, _("Fill if empty")),
(MODE_APPEND_LIST, _("Add to list")),
]
)
def __init__(self, pretix_fields, external_fields_id, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["pretix_field"] = forms.ChoiceField(
label=_("pretix Field"),
choices=pretix_fields_choices(pretix_fields),
required=False,
)
if external_fields_id:
self.fields["external_field"] = forms.ChoiceField(
widget=forms.Select(
attrs={
"data-model-select2": "json_script",
"data-select2-src": "#" + external_fields_id,
},
),
)
self.fields["external_field"].choices = [
(self["external_field"].value(), self["external_field"].value()),
]
print(self.fields)
class PropertyMappingFormSet(formset_factory(
PropertyMappingForm,
can_order=True,
can_delete=True,
extra=0,
)):
template_name = "pretixcontrol/datasync/property_mapping_formset.html"
def __init__(self, pretix_fields, external_fields, prefix, *args, **kwargs):
super().__init__(
form_kwargs={
"pretix_fields": pretix_fields,
"external_fields_id": prefix + "external-fields" if external_fields else None,
},
prefix=prefix,
*args, **kwargs)
self.external_fields = external_fields
def get_context(self):
ctx = super().get_context()
ctx["external_fields"] = self.external_fields
ctx["external_fields_id"] = self.prefix + "external-fields"
return ctx
def pretix_fields_choices(pretix_fields):
return [
(key, label + " [" + QUESTION_TYPE_IDENTIFIERS[ptype] + "]")
for (required_input, key, label, ptype, enum_opts, getter) in pretix_fields
]

View File

@@ -420,6 +420,23 @@ 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
@log_entry_types.new_from_dict({
"pretix.event.order.data_sync.failed": _("Error while transferring ticket data to {provider}:"),
})
class OrderDataSyncErrorLogentrytype(OrderLogEntryType):
def display(self, logentry, data):
errmes = data["error"]
if not isinstance(errmes, list):
errmes = [errmes]
return mark_safe(escape(self.plain) + "".join("<p>" + escape(msg) + "</p>" for msg in errmes))
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):

View File

@@ -0,0 +1,64 @@
{% load i18n %}
{% load eventurl %}
{% load bootstrap3 %}
{% load escapejson %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Data Sync" %}
</h3>
</div>
<div class="panel-body">
{{ test.hello }}
<table class="table table-condensed">
<tbody>
{% for pending in queued_sync_jobs %}
<tr>
<td>{{ pending.sync_provider }}</td>
<td>
{% if 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 }}
{% endblocktrans %}{% if pending.not_before %},
{% blocktrans trimmed with datetime=pending.not_before %}
waiting until {{ datetime }}
{% endblocktrans %}
{% endif %}
{% elif pending.not_before %}
{% blocktrans trimmed with datetime=pending.not_before %}
Waiting until {{ datetime }}
{% endblocktrans %}
{% else %}
<i class="fa fa-hourglass"></i> {% trans "Pending" %}
{% endif %}
</td>
<td>
<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">
{% csrf_token %}
{% 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>
{% endif %}
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-trash"></i> {% trans "Cancel" %}</button>
</form>
</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>
<input type="hidden" name="queue_sync" value="true">
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,77 @@
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load formset_tags %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for f in formset %}
{% bootstrap_form_errors f %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ f.id }}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field f.pretix_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field f.external_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field f.overwrite layout='inline' form_group_class="" %}
</div>
{{ f.value_map.as_hidden }}
<div class="col-md-2 text-right flip">
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
<i class="fa fa-edit"></i></button>
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field formset.empty_form.pretix_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field formset.empty_form.external_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field formset.empty_form.overwrite layout='inline' form_group_class="" %}
</div>
{{ f.value_map.as_hidden }}
<div class="col-md-2 text-right flip">
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
<i class="fa fa-edit"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add property" %}</button>
</p>
</div>
{% if external_fields %}
{{ external_fields|json_script:external_fields_id }}
{% endif %}

View File

@@ -36,6 +36,7 @@
from django.urls import include, re_path
from django.views.generic.base import RedirectView
from pretix.control.datasync import ControlSyncJob
from pretix.control.views import (
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
main, modelimport, oauth, orders, organizer, pdf, search, shredder,
@@ -428,6 +429,8 @@ urlpatterns = [
re_path(r'^orders/(?P<code>[0-9A-Z]+)/cancellationrequests/(?P<req>\d+)/delete$',
orders.OrderCancellationRequestDelete.as_view(),
name='event.order.cancellationrequests.delete'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/sync_job/(?P<provider>[^/]+)/$', ControlSyncJob.as_view(),
name='event.order.sync_job'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transactions/$', orders.OrderTransactions.as_view(), name='event.order.transactions'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
re_path(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),