Compare commits

..

29 Commits

Author SHA1 Message Date
Raphael Michel
d9d11883a1 Review notes 2025-02-21 15:39:29 +01:00
Raphael Michel
1acde9b8f9 Update src/pretix/base/models/log.py
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-02-11 15:40:42 +01:00
Raphael Michel
a9bf84b688 Improve efficiency of bulk operations 2025-02-11 10:56:37 +01:00
Raphael Michel
0079be68d3 Allow plugins to add data to the order API (Z#23179688) (#4822)
* Allow plugins to add data to the order API (Z#23179688)

* Update src/pretix/api/serializers/media.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Fix failing test

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-02-10 14:06:20 +01:00
Kian Cross
5bf6980a7b Improve date format heuristic for bank statement import (#4814)
When dates are ambiguous, use the event's locale to infer whether the
day or month comes first. Since regions follow certain date format
conventions, this helps make more accurate guesses.
2025-02-10 11:33:14 +01:00
Kian Cross
7e8ef47537 Add delete button to voucher details page (#4815) 2025-02-10 11:32:24 +01:00
Raphael Michel
86120d0296 Enable Finish language 2025-02-10 10:59:35 +01:00
Karoliina Grohn
e2086a8eca Translations: Update Finnish
Currently translated at 65.8% (3848 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
ad7c4a957d Translations: Update Finnish
Currently translated at 65.8% (3848 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Karoliina Grohn
f1fbb08c2b Translations: Update Finnish
Currently translated at 62.4% (3651 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
73b58cfb89 Translations: Update Finnish
Currently translated at 62.4% (3651 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
4323366ec3 Translations: Update Finnish
Currently translated at 58.1% (3401 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Karoliina Grohn
4d6b63e1c2 Translations: Update Finnish
Currently translated at 54.3% (3177 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
6edc12a89f Translations: Update Finnish
Currently translated at 54.3% (3177 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
e473d0bfce Translations: Update Finnish
Currently translated at 51.7% (3028 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
42469402b6 Translations: Update Finnish
Currently translated at 41.7% (2440 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
fd70d567e0 Translations: Update Finnish
Currently translated at 39.5% (2310 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
2b4d70fa30 Translations: Update Finnish
Currently translated at 37.7% (2207 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
조정화
3c27aa8591 Translations: Update Korean
Currently translated at 2.1% (125 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ko/

powered by weblate
2025-02-10 10:59:05 +01:00
Hannu Kaakinen
a1d6d636a8 Translations: Update Finnish
Currently translated at 36.9% (2159 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Johanna Ketola
373f9e666f Translations: Update Finnish
Currently translated at 36.9% (2159 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Karoliina Grohn
e524055249 Translations: Update Finnish
Currently translated at 36.9% (2159 of 5846 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-02-10 10:59:05 +01:00
Mira
85ae181ce8 Add missing return (#4823) 2025-02-10 10:54:56 +01:00
Raphael Michel
cfae2c62c3 OIDC: Fix incorrect error handling in server implementation 2025-02-07 18:04:46 +01:00
Mira
9c781a174d Fix Checkin logdisplay for deleted events (#4821) 2025-02-07 14:25:57 +01:00
Mira
f7cba6a2bd Fix log display for checkins and order splits (Z#23181229) (#4818)
- Fix link in pretix.event.order.changed.split
- Add link to existing order in pretix.event.order.changed.split_from
- Fix display of checkin entries without datetime in data
- Add additional info for admins (action type, linked content object)
2025-02-07 11:53:17 +01:00
Raphael Michel
c4436ca319 Change wording for "show_dates_on_frontpage" setting (#4798) 2025-02-06 14:55:15 +01:00
Martin Gross
c87401ef5d Widget: Prefill email_repeat with data-email attribute (Z#23181657) (#4813) 2025-02-06 12:07:43 +01:00
Raphael Michel
a53def0947 Fix CSRF token of admin mode button being removed if e.g. support menu is clicked (#4812) 2025-02-06 12:07:10 +01:00
26 changed files with 3083 additions and 2001 deletions

View File

@@ -109,6 +109,7 @@ cancellation_date datetime Time of order c
Will not be set for partial cancellations and is not
reliable for orders that have been cancelled,
reactivated and cancelled again.
plugin_data object Additional data added by plugins.
===================================== ========================== =======================================================
@@ -164,6 +165,10 @@ cancellation_date datetime Time of order c
The ``tax_code`` attribute has been added.
.. versionchanged:: 2025.2
The ``plugin_data`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -251,6 +256,7 @@ seat objects The assigned se
pdf_data object Data object required for ticket PDF generation. By default,
this field is missing. It will be added only if you add the
``pdf_data=true`` query parameter to your request.
plugin_data object Additional data added by plugins.
===================================== ========================== =======================================================
.. versionchanged:: 4.16
@@ -265,6 +271,10 @@ pdf_data object Data object req
The ``tax_code`` attribute has been added.
.. versionchanged:: 2025.2
The ``plugin_data`` attribute has been added.
.. _order-payment-resource:
Order payment resource
@@ -461,7 +471,8 @@ List of all orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
]
],
"plugin_data": {}
}
],
"downloads": [
@@ -483,7 +494,8 @@ List of all orders
}
],
"refunds": [],
"cancellation_date": null
"cancellation_date": null,
"plugin_data": {}
}
]
}
@@ -702,7 +714,8 @@ Fetching individual orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
]
],
"plugin_data": {}
}
],
"downloads": [
@@ -724,7 +737,8 @@ Fetching individual orders
}
],
"refunds": [],
"cancellation_date": null
"cancellation_date": null,
"plugin_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -1671,7 +1685,8 @@ List of all order positions
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
]
],
"plugin_data": {}
}
]
}
@@ -1798,7 +1813,8 @@ Fetching individual positions
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
]
],
"plugin_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -103,4 +103,4 @@ API
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile
:members: register_device_security_profile, order_api_details, orderposition_api_details

View File

@@ -123,7 +123,7 @@ LANGUAGES_RTL = {
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'fi', 'pt-br', 'gl',
'pt-br', 'gl',
}
LANGUAGES = ALL_LANGUAGES
LOCALE_PATHS = [

View File

@@ -46,6 +46,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
)
from pretix.api.signals import order_api_details, orderposition_api_details
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
@@ -494,6 +495,18 @@ class OrderPositionListSerializer(serializers.ListSerializer):
return data
class OrderPositionPluginDataField(serializers.Field):
def to_representation(self, value: OrderPosition):
d = {}
if value and value.pk:
for recv, resp in orderposition_api_details.send(
sender=self.context.get("event") or value.order.event,
orderposition=value
):
d.update(resp)
return d
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
print_logs = PrintLogSerializer(many=True, read_only=True)
@@ -504,6 +517,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
class Meta:
list_serializer_class = OrderPositionListSerializer
@@ -513,7 +527,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use', 'plugin_data')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
@@ -730,6 +744,18 @@ class OrderListSerializer(serializers.ListSerializer):
return data
class OrderPluginDataField(serializers.Field):
def to_representation(self, value: Order):
d = {}
if value and value.pk:
for recv, resp in order_api_details.send(
sender=self.context.get("event") or value.event,
order=value
):
d.update(resp)
return d
class OrderSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
invoice_address = InvoiceAddressSerializer(allow_null=True)
@@ -747,6 +773,7 @@ class OrderSerializer(I18nAwareModelSerializer):
queryset=SalesChannel.objects.none(),
required=False,
)
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
class Meta:
model = Order
@@ -755,7 +782,7 @@ class OrderSerializer(I18nAwareModelSerializer):
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',

View File

@@ -26,7 +26,7 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import periodic_task
from pretix.base.signals import EventPluginSignal, periodic_task
from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal()
@@ -43,6 +43,28 @@ return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurit
or a list of such instances.
"""
order_api_details = EventPluginSignal()
"""
Arguments: ``order``
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
should return a dictionary that is combined with the dictionaries of all other plugins.
Note that doing database or network queries in receivers to this signal is discouraged
and could cause serious performance issues. The main purpose is to provide information
from e.g. ``meta_info`` to the API consumer,
"""
orderposition_api_details = EventPluginSignal()
"""
Arguments: ``orderposition``
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
should return a dictionary that is combined with the dictionaries of all other plugins.
Note that doing database or network queries in receivers to this signal is discouraged
and could cause serious performance issues. The main purpose is to provide information
from e.g. ``meta_info`` to the API consumer,
"""
@receiver(periodic_task)
@scopes_disabled()

View File

@@ -37,7 +37,7 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db import connections, models
from django.utils.functional import cached_property
from pretix.base.logentrytype_registry import log_entry_types, make_link
@@ -165,6 +165,15 @@ class LogEntry(models.Model):
def delete(self, using=None, keep_parents=False):
raise TypeError("Logs cannot be deleted.")
@classmethod
def bulk_create_and_postprocess(cls, objects):
if connections['default'].features.can_return_rows_from_bulk_insert:
cls.objects.bulk_create(objects)
else:
for le in objects:
le.save()
cls.bulk_postprocess(objects)
@classmethod
def bulk_postprocess(cls, objects):
from pretix.api.webhooks import notify_webhooks

View File

@@ -1308,10 +1308,13 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"also affects a few other locations, however it should not be expected that the date of the "
"event is shown nowhere to users."),
label=_("This shop represents an event"),
help_text=_(
"Uncheck this box if you are only selling something that has no specific date, such as gift cards or a "
"ticket that can be used any time. The system will then stop showing the event date in some places like "
"the event start page. Note that pretix still is a system built around events and the date may still "
"show up in other places."
),
)
},
'show_date_to': {

View File

@@ -72,7 +72,7 @@ class OrderChangeLogEntryType(OrderLogEntryType):
prefix = _('The order has been changed:')
def display(self, logentry, data):
return self.prefix + ' ' + self.display_prefixed(logentry.event, logentry, data)
return format_html('{} {}', self.prefix, self.display_prefixed(logentry.event, logentry, data))
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return super().display(logentry, data)
@@ -282,12 +282,13 @@ class OrderChangedSplit(OrderChangeLogEntryType):
'organizer': event.organizer.slug,
'code': data['new_order']
})
return mark_safe(self.prefix + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
return format_html(
_('Position #{posid} ({old_item}, {old_price}) split into new order: {order}'),
old_item=escape(old_item),
posid=data.get('positionid', '?'),
order='<a href="{}">{}</a>'.format(url, data['new_order']),
order=format_html(mark_safe('<a href="{}">{}</a>'), url, data['new_order']),
old_price=money_filter(Decimal(data['old_price']), event.currency),
))
)
@log_entry_types.new()
@@ -295,8 +296,14 @@ class OrderChangedSplitFrom(OrderLogEntryType):
action_type = 'pretix.event.order.changed.split_from'
def display(self, logentry: LogEntry, data):
return _('This order has been created by splitting the order {order}').format(
order=data['original_order'],
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['original_order']
})
return format_html(
_('This order has been created by splitting the order {order}'),
order=format_html(mark_safe('<a href="{}">{}</a>'), url, data['original_order']),
)
@@ -318,7 +325,7 @@ class OrderChangedSplitFrom(OrderLogEntryType):
})
class CheckinErrorLogEntryType(OrderLogEntryType):
def display(self, logentry: LogEntry, data):
self.display_plain(self.plain, logentry, data)
return self.display_plain(self.plain, logentry, data)
def display_plain(self, plain, logentry: LogEntry, data):
if isinstance(plain, tuple):
@@ -330,7 +337,7 @@ class CheckinErrorLogEntryType(OrderLogEntryType):
event = logentry.event
if 'list' in data:
if 'list' in data and event:
try:
data['list'] = event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
@@ -343,12 +350,12 @@ class CheckinErrorLogEntryType(OrderLogEntryType):
if 'datetime' in data:
dt = dateutil.parser.parse(data.get('datetime'))
if abs((logentry.datetime - dt).total_seconds()) > 5 or 'forced' in data:
tz = event.timezone
data['datetime'] = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if abs((logentry.datetime - dt).total_seconds()) > 5 or data.get('forced'):
if event:
data['datetime'] = date_format(dt.astimezone(event.timezone), "SHORT_DATETIME_FORMAT")
return str(plain_with_dt).format_map(data)
else:
return str(plain_without_dt).format_map(data)
return str(plain_without_dt).format_map(data)
@log_entry_types.new('pretix.event.checkin')
@@ -410,7 +417,7 @@ class OrderPrintLogEntryType(OrderLogEntryType):
datetime=date_format(
dateutil.parser.parse(data["datetime"]).astimezone(logentry.event.timezone),
"SHORT_DATETIME_FORMAT"
),
) if logentry.event else data["datetime"],
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
)

View File

@@ -125,6 +125,15 @@
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
{% if voucher.pk %}
<div class="pull-left">
<a href="{% url "control:event.voucher.delete" organizer=request.organizer.slug event=request.event.slug voucher=voucher.pk %}"
class="btn btn-danger btn-lg">
<span class="fa fa-trash"></span>
{% trans "Delete voucher" %}
</a>
</div>
{% endif %}
</div>
{% endif %}
</form>

View File

@@ -110,7 +110,7 @@ class MessageView(TemplateView):
class LogDetailView(AdministratorPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
le = get_object_or_404(LogEntry, pk=request.GET.get('pk'))
return JsonResponse({'data': le.parsed_data})
return JsonResponse({'action_type': le.action_type, 'content_type': str(le.content_type), 'object_id': le.object_id, 'data': le.parsed_data})
class PaymentDetailView(AdministratorPermissionRequiredMixin, View):

View File

@@ -46,7 +46,7 @@ from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files import File
from django.db import connections, transaction
from django.db import transaction
from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch,
ProtectedError, Q, Subquery, Sum,
@@ -1159,13 +1159,7 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False)
)
if connections['default'].features.can_return_rows_from_bulk_insert:
LogEntry.objects.bulk_create(log_entries, batch_size=200)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)

View File

@@ -40,7 +40,7 @@ from dateutil.rrule import rruleset
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.files import File
from django.db import connections, transaction
from django.db import transaction
from django.db.models import Count, F, Prefetch, ProtectedError
from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory
@@ -657,24 +657,30 @@ class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View)
@transaction.atomic
def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'disable':
log_entries = []
for obj in self.get_queryset():
obj.log_action(
log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': False
}
)
}, save=False
))
obj.active = False
obj.save(update_fields=['active'])
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
elif request.POST.get('action') == 'enable':
log_entries = []
for obj in self.get_queryset():
obj.log_action(
log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': True
}
)
}, save=False
))
obj.active = True
obj.save(update_fields=['active'])
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
elif request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
@@ -682,22 +688,28 @@ class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View)
'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
})
elif request.POST.get('action') == 'delete_confirm':
log_entries = []
to_delete = []
for obj in self.get_queryset():
try:
if not obj.allow_delete():
raise ProtectedError('only deactivate', [obj])
CartPosition.objects.filter(addon_to__subevent=obj).delete()
obj.cartposition_set.all().delete()
obj.log_action('pretix.subevent.deleted', user=self.request.user)
obj.delete()
log_entries.append(obj.log_action('pretix.subevent.deleted', user=self.request.user, save=False))
to_delete.append(obj.pk)
except ProtectedError:
obj.log_action(
log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': False
}
)
}, save=False,
))
obj.active = False
obj.save(update_fields=['active'])
if to_delete:
CartPosition.objects.filter(addon_to__subevent_id__in=to_delete).delete()
CartPosition.objects.filter(subevent_id__in=to_delete).delete()
SubEvent.objects.filter(pk__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.'))
return redirect(self.get_success_url())
@@ -1009,13 +1021,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
f.save()
set_progress(90)
if connections['default'].features.can_return_rows_from_bulk_insert:
LogEntry.objects.bulk_create(log_entries)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
LogEntry.bulk_create_and_postprocess(log_entries)
self.request.event.cache.clear()
return len(subevents)
@@ -1578,13 +1584,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
self.save_itemvars()
self.save_meta()
if connections['default'].features.can_return_rows_from_bulk_insert:
LogEntry.objects.bulk_create(log_entries, batch_size=200)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
LogEntry.bulk_create_and_postprocess(log_entries)
self.request.event.cache.clear()
messages.success(self.request, _('Your changes have been saved.'))

View File

@@ -477,7 +477,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
log_entries.append(
v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False)
)
LogEntry.objects.bulk_create(log_entries)
LogEntry.bulk_create_and_postprocess(log_entries)
form.post_bulk_save(batch_vouchers)
batch_vouchers.clear()
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
@@ -619,19 +619,26 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
'forbidden': self.objects.exclude(redeemed=0),
})
elif request.POST.get('action') == 'delete_confirm':
log_entries = []
to_delete = []
for obj in self.objects:
if obj.allow_delete():
obj.log_action('pretix.voucher.deleted', user=self.request.user)
CartPosition.objects.filter(addon_to__voucher=obj).delete()
obj.cartposition_set.all().delete()
obj.delete()
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
to_delete.append(obj.pk)
else:
obj.log_action('pretix.voucher.changed', user=self.request.user, data={
log_entries.append(obj.log_action('pretix.voucher.changed', user=self.request.user, data={
'max_usages': min(obj.redeemed, obj.max_usages),
'bulk': True
})
}), save=False)
obj.max_usages = min(obj.redeemed, obj.max_usages)
obj.save(update_fields=['max_usages'])
if to_delete:
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
Voucher.objects.filter(pk__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
return redirect(self.get_success_url())

View File

@@ -49,7 +49,7 @@ from django.utils.translation import gettext_lazy as _, pgettext
from django.views import View
from django.views.generic import ListView
from pretix.base.models import Item, Quota, WaitingListEntry
from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import assign_automatically
from pretix.base.views.tasks import AsyncAction
@@ -160,10 +160,15 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
'forbidden': self.get_queryset().filter(voucher__isnull=False),
})
elif request.POST.get('action') == 'delete_confirm':
for obj in self.get_queryset(force_filtered=True):
if not obj.voucher_id:
obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user)
obj.delete()
with transaction.atomic():
log_entries = []
to_delete = []
for obj in self.get_queryset(force_filtered=True):
if not obj.voucher_id:
log_entries.append(obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user, save=False))
to_delete.append(obj.pk)
WaitingListEntry.objects.filter(id__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, _('The selected entries have been deleted.'))
return self._redirect_back()
@@ -186,16 +191,17 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
if 'move_top' in request.POST:
try:
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_top'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1
wle.save(update_fields=['priority'])
wle.log_action(
'pretix.event.orders.waitinglist.changed',
data={'priority': wle.priority},
user=self.request.user,
)
with transaction.atomic():
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_top'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1
wle.save(update_fields=['priority'])
wle.log_action(
'pretix.event.orders.waitinglist.changed',
data={'priority': wle.priority},
user=self.request.user,
)
messages.success(request, _('The waiting list entry has been moved to the top.'))
return self._redirect_back()
except WaitingListEntry.DoesNotExist:
@@ -204,16 +210,17 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
if 'move_end' in request.POST:
try:
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_end'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1
wle.save(update_fields=['priority'])
wle.log_action(
'pretix.event.orders.waitinglist.changed',
data={'priority': wle.priority},
user=self.request.user,
)
with transaction.atomic():
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_end'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1
wle.save(update_fields=['priority'])
wle.log_action(
'pretix.event.orders.waitinglist.changed',
data={'priority': wle.priority},
user=self.request.user,
)
messages.success(request, _('The waiting list entry has been moved to the end of the list.'))
return self._redirect_back()
except WaitingListEntry.DoesNotExist:

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2025-02-05 20:00+0000\n"
"PO-Revision-Date: 2025-02-06 18:00+0000\n"
"Last-Translator: 조정화 <junghwa.jo@om.org>\n"
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
">\n"
@@ -516,52 +516,53 @@ msgstr "이벤트 생성"
#: pretix/api/webhooks.py:329
msgid "Event details changed"
msgstr ""
msgstr "이벤트 세부 정보가 변경되었습니다"
#: pretix/api/webhooks.py:333
msgid "Event deleted"
msgstr ""
msgstr "이벤트 삭제"
#: pretix/api/webhooks.py:337
msgctxt "subevent"
msgid "Event series date added"
msgstr ""
msgstr "이벤트 시리즈 날짜 추가"
#: pretix/api/webhooks.py:341
msgctxt "subevent"
msgid "Event series date changed"
msgstr ""
msgstr "이벤트 시리즈 날짜 변경"
#: pretix/api/webhooks.py:345
msgctxt "subevent"
msgid "Event series date deleted"
msgstr ""
msgstr "이벤트 시리즈 날짜 삭제"
#: pretix/api/webhooks.py:349
msgid ""
"Product changed (including product added or deleted and including changes to "
"nested objects like variations or bundles)"
msgstr ""
msgstr "제품 변경(제품 추가 또는 삭제, 변형 또는 번들과 같은 중첩된 객체에 대한 변경 "
"포함)"
#: pretix/api/webhooks.py:354
msgid "Shop taken live"
msgstr ""
msgstr "라이브 촬영 상점"
#: pretix/api/webhooks.py:358
msgid "Shop taken offline"
msgstr ""
msgstr "오프라인 쇼핑"
#: pretix/api/webhooks.py:362
msgid "Test-Mode of shop has been activated"
msgstr ""
msgstr "상점의 테스트 모드가 활성화되었습니다"
#: pretix/api/webhooks.py:366
msgid "Test-Mode of shop has been deactivated"
msgstr ""
msgstr "상점의 테스트 모드가 비활성화되었습니다"
#: pretix/api/webhooks.py:370
msgid "Waiting list entry added"
msgstr ""
msgstr "대기자 명단 항목 추가"
#: pretix/api/webhooks.py:374
msgid "Waiting list entry changed"

View File

@@ -292,11 +292,11 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N
trans.save()
def parse_date(date_str):
def parse_date(date_str, region=None):
try:
return dateutil.parser.parse(
date_str,
dayfirst="." in date_str,
dayfirst="." in date_str or region in ["GB"],
).date()
except (ValueError, OverflowError):
pass
@@ -339,7 +339,7 @@ def _get_unknown_transactions(job: BankImportJob, data: list, event: Event = Non
external_id=row.get('external_id'),
currency=event.currency if event else job.currency)
trans.date_parsed = parse_date(trans.date)
trans.date_parsed = parse_date(trans.date, (event and event.settings.region) or (organizer and organizer.settings.region) or None)
trans.checksum = trans.calculate_checksum()
if trans.checksum not in known_checksums and (not trans.external_id or (trans.external_id, trans.date, trans.amount) not in known_by_external_id):

View File

@@ -244,15 +244,16 @@ class AuthorizeView(View):
response_mode, state)
if "id_token_hint" in request_data:
self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
redirect_uri, response_mode, state)
return self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
redirect_uri, response_mode, state)
has_valid_session = bool(request.customer)
if has_valid_session and max_age:
try:
has_valid_session = int(time.time() - get_customer_auth_time(request)) < int(max_age)
except ValueError:
self._redirect_error("invalid_request", "invalid max_age value", redirect_uri, response_mode, state)
return self._redirect_error("invalid_request", "invalid max_age value", redirect_uri,
response_mode, state)
if not has_valid_session and prompt and prompt == "none":
return self._redirect_error("interaction_required", "user is not logged in but no prompt is allowed",

View File

@@ -1065,11 +1065,17 @@ function add_log_expand_handlers(el) {
} else if ($a.is("[data-expandpayment]")) {
url += 'payment/'
}
function format_data(data) {
return Object.entries(data).map(([key, value]) =>
$("<div>").append(
$("<b>").text(key + ': '),
$("<span>").text(JSON.stringify(value, null, 2))));
}
$.getJSON(url + '?pk=' + id, function (data) {
if ($a.parent().tagName === "p") {
$("<pre>").text(JSON.stringify(data.data, null, 2)).insertAfter($a.parent());
$("<pre>").append(format_data(data)).insertAfter($a.parent());
} else {
$("<pre>").text(JSON.stringify(data.data, null, 2)).appendTo($a.parent());
$("<pre>").append(format_data(data)).appendTo($a.parent());
}
$a.remove();
});

View File

@@ -1,6 +1,6 @@
/*global $,u2f */
$(function () {
$('.sidebar .dropdown, ul.navbar-nav .dropdown, .navbar-events-collapse').on('shown.bs.collapse shown.bs.dropdown', function () {
$('.context-selector.dropdown').on('shown.bs.collapse shown.bs.dropdown', function () {
$(this).parent().find("input").val("").trigger('forceRunQuery').focus();
});
$('.dropdown-menu .form-box input').click(function (e) {

View File

@@ -162,6 +162,7 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
"tax_rule": None,
"pseudonymization_id": op.pseudonymization_id,
"pdf_data": {},
"plugin_data": {},
"seat": None,
"canceled": False,
"valid_from": None,

View File

@@ -477,7 +477,8 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
'zipcode': None,
'state': None,
'country': None,
'canceled': False
'canceled': False,
'plugin_data': {},
}
],
'downloads': [],
@@ -487,7 +488,8 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
'refunds': [],
'require_approval': False,
'sales_channel': 'web',
'cancellation_date': None
'cancellation_date': None,
'plugin_data': {},
}
@@ -533,13 +535,15 @@ def test_order_create_positionids_addons_simulated(token_client, organizer, even
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None,
'addon_to': None, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id,
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None},
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None,
'plugin_data': {}},
{'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00',
'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None,
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None,
'addon_to': 1, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id,
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None,
'plugin_data': {}}
]

View File

@@ -236,6 +236,7 @@ TEST_ORDERPOSITION_RES = {
],
"subevent": None,
"canceled": False,
"plugin_data": {},
}
TEST_PAYMENTS_RES = [
{
@@ -333,6 +334,7 @@ TEST_ORDER_RES = {
"payments": TEST_PAYMENTS_RES,
"refunds": TEST_REFUNDS_RES,
"cancellation_date": None,
"plugin_data": {},
}

View File

@@ -203,7 +203,8 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
"canceled": False,
"valid_from": None,
"valid_until": None,
"blocked": None
"blocked": None,
"plugin_data": {},
}
assert resp.data["linked_giftcard"] == {
"id": giftcard.pk,

View File

@@ -790,3 +790,33 @@ def test_ignore_by_external_id(env, job):
}])
with scopes_disabled():
assert BankTransaction.objects.count() == 2
@pytest.mark.django_db
def test_ambigious_date_without_region(env, job):
process_banktransfers(job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY1Z3AS',
'date': '03/05/2016',
'amount': '23.00'
}])
env[2].refresh_from_db()
with scopes_disabled():
assert env[2].payments.last().info_data["date"] == "2016-03-05"
@pytest.mark.django_db
def test_ambigious_date_with_region(env, job):
env[0].settings.region = "GB"
process_banktransfers(job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY1Z3AS',
'date': '03/05/2016',
'amount': '23.00'
}])
env[2].refresh_from_db()
with scopes_disabled():
assert env[2].payments.last().info_data["date"] == "2016-05-03"

View File

@@ -40,3 +40,5 @@ def test_date_formats():
assert dt == parse_date("2020-07-01")
assert dt == parse_date("2020-7-1")
assert dt == parse_date("01/07/2020", "GB")