Compare commits

...
Author SHA1 Message Date
Raphael Michel 943fe8b185 API: Disable endpoints that should have never existed 2026-07-28 13:59:23 +02:00
Raphael Michel 16040f70bb [SECURITY] Add missing permission check for view (CVE-2026-57532) 2026-07-28 12:24:59 +02:00
Raphael Michel 9b5ed06b2e Improve handling of reauth during async tasks (Z#23238978) (#6381) 2026-07-28 10:59:42 +02:00
Raphael Michel f3ffce4e5b Question form: Allow to unset required boolean questions (Z#23239887) (#6382)
Proper fix for #6366
2026-07-28 10:59:34 +02:00
Albizuri 5c98dc16b7 Translations: Update Basque
Currently translated at 48.3% (3077 of 6362 strings)

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

powered by weblate
2026-07-28 10:11:51 +02:00
Nikolai d814b40fcd Translations: Update Danish
Currently translated at 61.8% (3932 of 6362 strings)

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

powered by weblate
2026-07-28 10:11:51 +02:00
LilaHexeandRaphael Michel a696c493e2 PayPal: Drop maximum length of client ID (#6424)
* Update PayPal max_length to 82 characters

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-07-28 09:13:45 +02:00
dependabot[bot] e88b539375 Update tlds requirement from >=2026041800 to >=2026072401 (#6428)
Updates the requirements on [tlds](https://github.com/kichik/tlds) to permit the latest version.
- [Commits](https://github.com/kichik/tlds/commits)

---
updated-dependencies:
- dependency-name: tlds
  dependency-version: '2026072401'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 09:08:29 +02:00
Raphael Michel 163cf85c86 Orders: Fix N+1 query issues on order detail and change page (#6390)
* rders: Fix N+1 query issues on order detail and change page

There is one I couldn't fix: Loading the list of tax rules for every
select box on the OrderChange page. Unfortunately, Django has a
cache-breaking .all() in ModelChoiceField and that would need nasty
patching that didn't feel worth it

* Fix isort

* Fix N+1 query in gate call

* Fix leftovers

* Add local cache for objects referenced in log entries

* Update src/pretix/control/views/orders.py
2026-07-28 09:08:14 +02:00
pajowu 55b6887c5e Show new login time in users timezone (Z#23241469) (#6420) 2026-07-28 09:08:00 +02:00
dependabot[bot] 2592d3cb8d Update django-phonenumber-field requirement from ==8.4.* to ==8.5.* (#6415)
Updates the requirements on [django-phonenumber-field](https://github.com/django-phonenumber-field/django-phonenumber-field) to permit the latest version.
- [Release notes](https://github.com/django-phonenumber-field/django-phonenumber-field/releases)
- [Changelog](https://github.com/django-phonenumber-field/django-phonenumber-field/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/django-phonenumber-field/django-phonenumber-field/compare/8.4.0...8.5.0)

---
updated-dependencies:
- dependency-name: django-phonenumber-field
  dependency-version: 8.5.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 09:07:55 +02:00
Raphael Michel 47b632eabe RejectInvalidInputMiddleware: Ignore invalid charsets on body (#6314) 2026-07-28 09:07:16 +02:00
Raphael Michel 7f2e957092 Security profiles: Ensure default profile is always listed first (#6266) 2026-07-28 09:06:59 +02:00
Raphael Michel 1ff9742a08 Customer export: Add memberships tab (Z#23228422) (#6253) 2026-07-28 09:06:45 +02:00
Raphael Michel d9f8679647 Waiting list: Redirect back after voucher edit (Z#23230178) (#6252) 2026-07-27 23:19:10 +02:00
Raphael Michelandpajowu 1a7e36a144 Event-dashboard: Lazy-load warnings (#6421)
* Event-dashboard: Lazy-load warnings

I don't like this, it's not pretty, but we have no quick way of fixing
the overpaid orders check otherwise and a slow query on the event
dashboard feels really bad, because even if you don't wanna see the
dashboard ou are always going through it in navigation and that can get
very annoying very quickly.

* Update src/pretix/control/templates/pretixcontrol/event/dashboard_partial_warnings.html

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-07-27 14:40:34 +02:00
Raphael Michelandpajowu 9afc71c245 Update src/pretix/control/views/checkin.py
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-07-27 13:54:06 +02:00
Raphael Michel 1087db9529 Check-in history: Avoid JOINs
On a test dataset, this was a speedup from 18s to 900ms
2026-07-27 13:54:06 +02:00
Kara Engelhardt 6d62284d87 Add global id field to order 2026-07-27 13:22:42 +02:00
szurofkamarciidfbe08444ef04788 db147280fd Translations: Update Hungarian
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-24 13:45:01 +02:00
szurofkamarciidfbe08444ef04788 d1ce4566a7 Translations: Update Hungarian
Currently translated at 63.8% (4060 of 6362 strings)

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

powered by weblate
2026-07-24 13:45:01 +02:00
dependabot[bot] c3cfd4ea91 Update fakeredis requirement from ==2.36.* to ==2.37.*
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.36.0...v2.37.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-version: 2.37.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 13:43:01 +02:00
Raphael Michel 92e7069a8e Improve performance of overpayment checks (Z#23241035) (#6410)
* Improve performance of overpayment checks

* Upgrade django-scopes
2026-07-24 13:42:33 +02:00
dependabot[bot] f471091d77 Bump immutable from 5.1.5 to 5.1.9 (#6417)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.5 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:18:20 +02:00
Raphael Michel 351d2055f8 Log view: Disable counting the number of pages (Z#23241035) (#6414) 2026-07-24 12:17:48 +02:00
33 changed files with 7872 additions and 5109 deletions
+3 -3
View File
@@ -2584,9 +2584,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"dev": true,
"license": "MIT"
},
+4 -4
View File
@@ -52,10 +52,10 @@ dependencies = [
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
"django-phonenumber-field==8.4.*",
"django-phonenumber-field==8.5.*",
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-scopes==2.0.*",
"django-scopes==2.1.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
"dnspython==2.8.*",
@@ -98,7 +98,7 @@ dependencies = [
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2026041800",
"tlds>=2026072401",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
@@ -112,7 +112,7 @@ dev = [
"aiohttp==3.14.*",
"coverage",
"coveralls",
"fakeredis==2.36.*",
"fakeredis==2.37.*",
"flake8==7.3.*",
"freezegun",
"isort==8.0.*",
+13 -4
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
@@ -52,10 +51,18 @@ class BaseSecurityProfile:
"""
raise NotImplementedError()
@property
def priority(self) -> int:
"""
Priority for ordering, higher will come first.
"""
return 100
class FullAccessSecurityProfile(BaseSecurityProfile):
identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
priority = 1000
def is_allowed(self, request):
return True
@@ -195,13 +202,15 @@ def get_all_security_profiles():
if _ALL_PROFILES:
return _ALL_PROFILES
types = OrderedDict()
types = []
for recv, ret in register_device_security_profile.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
types.append(r)
else:
types[ret.identifier] = ret
types.append(ret)
types.sort(key=lambda el: el.priority, reverse=True)
types = {r.identifier: r for r in types}
_ALL_PROFILES = types
return types
+2 -1
View File
@@ -45,6 +45,7 @@ from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
from rest_framework.generics import get_object_or_404
from rest_framework.mixins import UpdateModelMixin
from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
@@ -711,7 +712,7 @@ class SeatFilter(FilterSet):
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = SeatSerializer
queryset = Seat.objects.none()
write_permission = 'event.settings.general:write'
+57 -3
View File
@@ -40,11 +40,12 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..exporter import MultiSheetListExporter, OrganizerLevelExportMixin
from ..models import Membership
from ..signals import register_multievent_data_exporters
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
identifier = 'customerlist'
verbose_name = gettext_lazy('Customer accounts')
category = pgettext_lazy('export_category', 'Customer accounts')
@@ -54,13 +55,20 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
def get_required_organizer_permission(cls) -> str:
return 'organizer.customers:write'
@property
def sheets(self):
return (
('customers', _('Customers')),
('memberships', _('Memberships')),
)
@property
def additional_form_fields(self):
return OrderedDict(
[]
)
def iterate_list(self, form_data):
def iterate_customers(self, form_data):
qs = self.organizer.customers.prefetch_related('provider')
headers = [
@@ -109,6 +117,52 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
]
yield row
def iterate_memberships(self, form_data):
qs = Membership.objects.filter(
customer__organizer=self.organizer
).prefetch_related('membership_type').select_related('customer', 'granted_in', 'granted_in__order')
headers = [
_('Customer ID'),
_('External identifier'),
_('Email'),
_('Test mode'),
_('Canceled'),
_('Membership type'),
_('Purchase ticket'),
_('Start date'),
_('Start time'),
_('End date'),
_('End time'),
_('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Name') + ': ' + str(label))
yield headers
tz = get_current_timezone()
for obj in qs:
row = [
obj.customer.identifier,
obj.customer.external_identifier,
obj.customer.email or '',
_('Yes') if obj.testmode else _('No'),
_('Yes') if obj.canceled else _('No'),
str(obj.membership_type.name),
f'{obj.granted_in.order.code}-{obj.granted_in.positionid}' if obj.granted_in else None,
obj.date_start.astimezone(tz).strftime('%Y-%m-%d'),
obj.date_start.astimezone(tz).strftime('%H:%M'),
obj.date_end.astimezone(tz).strftime('%Y-%m-%d'),
obj.date_end.astimezone(tz).strftime('%H:%M'),
obj.attendee_name or '',
]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(obj.attendee_name_parts.get(k, ''))
yield row
def get_filename(self):
return '{}_customers'.format(self.organizer.slug)
+1 -1
View File
@@ -1121,7 +1121,7 @@ class BaseQuestionsForm(forms.Form):
for q in question_cache.values():
if q.required and q.type == Question.TYPE_BOOLEAN:
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
del d['question_%d' % q.pk]
d['question_%d' % q.pk] = None
return d
+10 -2
View File
@@ -497,8 +497,16 @@ class RejectInvalidInputMiddleware(MiddlewareMixin):
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
raise BadRequest("Invalid characters in input.")
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
if any("\x00" in value for key, value_list in request.POST.lists() for value in value_list):
raise BadRequest("Invalid characters in input.")
try:
post_data = request.POST.lists()
except BadRequest:
# Reading request.POST wasn't possible, probably an invalid charset. Django will crash once we actually
# use request.POST, but if we don't, let's not crash it (required for some weird payment provider
# webhooks, e.g. computop).
pass
else:
if any("\x00" in value for key, value_list in post_data for value in value_list):
raise BadRequest("Invalid characters in input.")
class CustomCommonMiddleware(CommonMiddleware):
+26 -4
View File
@@ -508,20 +508,20 @@ class Order(LockModel, LoggedModel):
@classmethod
def annotate_overpayments(cls, qs, results=True, refunds=True, sums=False):
payment_sum = OrderPayment.objects.filter(
payment_sum = OrderPayment.objects.with_scopes_disabled().filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
order=OuterRef('pk')
).order_by().values('order').annotate(s=Sum('amount')).values('s')
refund_sum = OrderRefund.objects.filter(
refund_sum = OrderRefund.objects.with_scopes_disabled().filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED),
order=OuterRef('pk')
).order_by().values('order').annotate(s=Sum('amount')).values('s')
external_refund = OrderRefund.objects.filter(
external_refund = OrderRefund.objects.with_scopes_disabled().filter(
state=OrderRefund.REFUND_STATE_EXTERNAL,
order=OuterRef('pk')
)
pending_refund = OrderRefund.objects.filter(
pending_refund = OrderRefund.objects.with_scopes_disabled().filter(
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
order=OuterRef('pk')
)
@@ -2072,6 +2072,17 @@ class OrderPayment(models.Model):
"""
return '{}-P-{}'.format(self.order.code, self.local_id)
@property
def global_id(self):
"""
The global ID of this payment, constructed by the organizer slug, event slug, and the full id.
"""
return "{organizer}-{event}-{full_id}".format(
organizer=self.order.organizer.slug.upper(),
event=self.order.event.slug.upper(),
full_id=self.full_id,
)
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
@@ -2272,6 +2283,17 @@ class OrderRefund(models.Model):
"""
return '{}-R-{}'.format(self.order.code, self.local_id)
@property
def global_id(self):
"""
The global ID of this refund, constructed by the organizer slug, event slug, and the full id.
"""
return "{organizer}-{event}-{full_id}".format(
organizer=self.order.organizer.slug.upper(),
event=self.order.event.slug.upper(),
full_id=self.full_id,
)
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
+1 -3
View File
@@ -562,9 +562,7 @@ class OrderPositionChangeForm(forms.Form):
if instance.addon_to_id:
del self.fields['operation_split']
if not instance.seat and not (
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
):
if not instance.seat and not instance._seat_allowed:
del self.fields['seat']
choices = [
+21 -13
View File
@@ -33,6 +33,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import functools
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
@@ -69,6 +70,15 @@ OVERVIEW_BANLIST = [
]
@functools.lru_cache(maxsize=32)
def object_id_to_string(model_class, **kwargs):
# The cache is thread-local and persists requests, but it's small enough that we can accept that
try:
return str(model_class.objects.get(**kwargs))
except model_class.DoesNotExist:
return "?"
class OrderChangeLogEntryType(OrderLogEntryType):
prefix = _('The order has been changed:')
@@ -93,10 +103,10 @@ class OrderItemChanged(OrderChangeLogEntryType):
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
old_item += ' - ' + str(object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['old_variation']))
new_item = str(event.items.get(pk=data['new_item']))
if data['new_variation']:
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
new_item += ' - ' + str(object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['new_variation']))
return _('Position #{posid}: {old_item} ({old_price}) changed to {new_item} ({new_price}).').format(
posid=data.get('positionid', '?'),
old_item=old_item, new_item=new_item,
@@ -153,14 +163,14 @@ class OrderTaxRuleChanged(OrderChangeLogEntryType):
if 'positionid' in data:
return _('Tax rule of position #{posid} changed from {old_rule} to {new_rule}.').format(
posid=data.get('positionid', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
old_rule=object_id_to_string(TaxRule, pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=object_id_to_string(TaxRule, pk=data['new_taxrule']),
)
elif 'fee' in data:
return _('Tax rule of fee #{fee} changed from {old_rule} to {new_rule}.').format(
fee=data.get('fee', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
old_rule=object_id_to_string(TaxRule, pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=object_id_to_string(TaxRule, pk=data['new_taxrule']),
)
@@ -204,7 +214,7 @@ class OrderCanceled(OrderChangeLogEntryType):
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
old_item += ' - ' + object_id_to_string(ItemVariation, pk=data['old_variation'])
return _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
posid=data.get('positionid', '?'),
old_item=old_item,
@@ -219,7 +229,7 @@ class OrderPositionAdded(OrderChangeLogEntryType):
def display_prefixed(self, event: Event, logentry: LogEntry, data):
item = str(event.items.get(pk=data['item']))
if data['variation']:
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
item += ' - ' + object_id_to_string(ItemVariation, item__event_id=event.pk, pk=data['variation'])
if data['addon_to']:
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
return _('Position #{posid} created: {item} ({price}) as an add-on to position #{addon_to}.').format(
@@ -283,7 +293,7 @@ class OrderChangedSplit(OrderChangeLogEntryType):
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
old_item += ' - ' + object_id_to_string(ItemVariation, pk=data['old_variation'])
url = reverse('control:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
@@ -339,6 +349,7 @@ class OrderChangedSplitFrom(OrderLogEntryType):
'pretix.event.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
})
class CheckinErrorLogEntryType(OrderLogEntryType):
def display(self, logentry: LogEntry, data):
return self.display_plain(self.plain, logentry, data)
@@ -353,10 +364,7 @@ class CheckinErrorLogEntryType(OrderLogEntryType):
event = logentry.event
if 'list' in data and event:
try:
data['list'] = event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
data['list'] = _("(unknown)")
data['list'] = object_id_to_string(CheckinList, event_id=event.id, pk=data['list'])
else:
data['list'] = _("(unknown)")
@@ -0,0 +1,58 @@
{% load i18n %}
{% if has_overpaid_orders %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
You should review the cases and consider refunding the overpaid amount to the user.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
</div>
{% endif %}
{% if has_pending_refunds %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending refunds</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
</div>
{% endif %}
{% if has_cancellation_requests %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>requested cancellations</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
</div>
{% endif %}
{% if has_pending_approvals %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending approvals</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
</div>
{% endif %}
{% if has_pending_orders_with_full_payment %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
because no quota was left at the time their payment arrived. You should review the cases and consider
either refunding the customer or creating more space.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
class="btn btn-primary">{% trans "Show affected orders" %}</a>
</div>
{% endif %}
{% if has_sync_problems %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
Orders in this event could not be <strong>synced to an external system</strong> as configured.
{% endblocktrans %}
<a href="{% url "control:event.datasync.failedjobs" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show sync problems" %}</a>
</div>
{% endif %}
@@ -14,6 +14,7 @@
{% else %}
{{ request.event.get_date_range_display }}
{% endif %}
<span id="warnings_loading" class="fa fa-cog fa-spin"></span>
</small>
</h1>
<div class="helper-space-below">
@@ -31,63 +32,7 @@
</div>
<div class="clearfix"></div>
</div>
{% if has_overpaid_orders %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
You should review the cases and consider refunding the overpaid amount to the user.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
</div>
{% endif %}
{% if has_pending_refunds %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending refunds</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
</div>
{% endif %}
{% if has_cancellation_requests %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>requested cancellations</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
</div>
{% endif %}
{% if has_pending_approvals %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending approvals</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
</div>
{% endif %}
{% if has_pending_orders_with_full_payment %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
because no quota was left at the time their payment arrived. You should review the cases and consider
either refunding the customer or creating more space.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
class="btn btn-primary">{% trans "Show affected orders" %}</a>
</div>
{% endif %}
{% if has_sync_problems %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
Orders in this event could not be <strong>synced to an external system</strong> as configured.
{% endblocktrans %}
<a href="{% url "control:event.datasync.failedjobs" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show sync problems" %}</a>
</div>
{% endif %}
<div id="warnings_target"></div>
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
{% if request.event.has_subevents %}
@@ -66,5 +66,5 @@
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% include "pretixcontrol/pagination_huge.html" %}
{% endblock %}
@@ -833,7 +833,7 @@
<strong>{% trans "Pending total" %}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
<strong>{{ order.pending_sum|money:event.currency }}</strong>
<strong>{{ pending_sum|money:event.currency }}</strong>
</div>
<div class="clearfix"></div>
</div>
@@ -64,5 +64,5 @@
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% include "pretixcontrol/pagination_huge.html" %}
{% endblock %}
@@ -251,7 +251,7 @@
</td>
<td>
{% if e.voucher %}
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}">
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}?next={{ request.get_full_path|urlencode }}">
{{ e.voucher }}
</a>
{% elif not e.voucher and e.availability.0 == 100 and e.availability.1|default_if_none:"none" != "none" %}
+2 -1
View File
@@ -273,7 +273,8 @@ urlpatterns = [
re_path(r'^$', dashboards.event_index, name='event.index'),
re_path(r'^qrcode.(?P<filetype>(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
re_path(r'^dashboard/partials/logs$', dashboards.event_index_log_lazy, name='event.index.logs'),
re_path(r'^dashboard/partials/warnings$', dashboards.event_index_warnings_lazy, name='event.index.warnings'),
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
re_path(r'^transfer_session/$', event.EventTransferSession.as_view(), name='event.transfer_session'),
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
+10 -3
View File
@@ -484,10 +484,17 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_queryset(self):
qs = Checkin.all.filter(
list__event=self.request.event,
).select_related(
'position', 'position__order', 'position__item', 'position__variation', 'position__subevent'
list_id__in=self.request.event.checkin_lists.values_list('id', flat=True),
).prefetch_related(
# For events with huge numbers of check-ins, prefetch_related is a lot more efficient than
# select_related, since it just needs to query one page size of positions instead of joining
# multiple huge tables during result computation
Prefetch(
'position',
queryset=OrderPosition.all.select_related(
'order', 'item', 'variation', 'subevent',
)
),
'list', 'gate', 'device'
)
if self.filter_form.is_valid():
+42 -30
View File
@@ -379,6 +379,42 @@ def event_index(request, organizer, event):
'comment_form': CommentForm(initial={'comment': request.event.comment}, readonly=not can_change_event_settings),
}
ctx['timeline'] = [
{
'date': t.datetime.astimezone(request.event.timezone).date(),
'entry': t,
'time': t.datetime.astimezone(request.event.timezone)
}
for t in timeline_for_event(request.event, subevent)
]
ctx['today'] = now().astimezone(request.event.timezone).date()
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
resp = render(request, 'pretixcontrol/event/index.html', ctx)
return resp
def event_index_widgets_lazy(request, organizer, event):
subevent = None
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
i = request.GET.get("subevent", "")
try:
subevent = request.event.subevents.get(pk=i)
except SubEvent.DoesNotExist:
pass
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return build_json_response(widgets)
def event_index_warnings_lazy(request, organizer, event):
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
request=request)
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
'event.settings.general:write', request=request)
ctx = {}
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
@@ -402,35 +438,11 @@ def event_index(request, organizer, event):
| Q(failed_attempts__gt=0)
).exists()
ctx['timeline'] = [
{
'date': t.datetime.astimezone(request.event.timezone).date(),
'entry': t,
'time': t.datetime.astimezone(request.event.timezone)
}
for t in timeline_for_event(request.event, subevent)
]
ctx['today'] = now().astimezone(request.event.timezone).date()
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
resp = render(request, 'pretixcontrol/event/index.html', ctx)
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
return resp
def event_index_widgets_lazy(request, organizer, event):
subevent = None
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
i = request.GET.get("subevent", "")
try:
subevent = request.event.subevents.get(pk=i)
except SubEvent.DoesNotExist:
pass
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return build_json_response(widgets)
return render(
request,
'pretixcontrol/event/dashboard_partial_warnings.html',
ctx
)
def event_index_log_lazy(request, organizer, event):
@@ -465,7 +477,7 @@ def event_index_log_lazy(request, organizer, event):
return render(
request,
'pretixcontrol/event/logs_embed.html',
'pretixcontrol/event/dashboard_partial_logs.html',
{
'logs': qs[:5]
}
+3 -2
View File
@@ -121,7 +121,7 @@ from ...helpers.format import (
)
from ..forms.filter import LogFilterForm
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
from . import CreateView, LargeResultSetPaginator, PaginationMixin, UpdateView
logger = logging.getLogger(__name__)
@@ -1254,6 +1254,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/event/logs.html'
model = LogEntry
paginator_class = LargeResultSetPaginator
context_object_name = 'logs'
def get_queryset(self):
@@ -1567,7 +1568,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
return ctx
class QuickSetupView(FormView):
class QuickSetupView(EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/event/quick_setup.html'
permission = 'event.settings.general:write'
form_class = QuickSetupForm
+30 -11
View File
@@ -81,7 +81,7 @@ from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
ScheduledEventExport, generate_secret,
ScheduledEventExport, SeatCategoryMapping, generate_secret,
)
from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
@@ -564,10 +564,11 @@ class OrderDetail(OrderView):
})
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
ctx['overpaid'] = self.order.pending_sum * -1
pending_sum = self.order.pending_sum
ctx['overpaid'] = pending_sum * -1
ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum
ctx['pending_sum'] = pending_sum
ctx['uncancelled_invoice'] = self.order.invoices.exclude(
Exists(self.order.invoices.filter(refers=OuterRef('pk'), is_cancellation=True))
).exclude(is_cancellation=True).first()
@@ -601,8 +602,9 @@ class OrderDetail(OrderView):
).prefetch_related(
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
Prefetch('all_checkins', queryset=Checkin.all.select_related('list', 'gate').order_by('datetime')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device').order_by('datetime')),
Prefetch('subevent', queryset=self.request.event.subevents.all()),
).order_by('positionid')
positions = []
@@ -1985,20 +1987,37 @@ class OrderChange(OrderView):
def fees(self):
fees = list(self.order.fees.all())
for f in fees:
f.form = OrderFeeChangeForm(prefix='of-{}'.format(f.pk), instance=f,
data=self.request.POST if self.request.method == "POST" else None)
f.form = OrderFeeChangeForm(
prefix='of-{}'.format(f.pk),
instance=f,
data=self.request.POST if self.request.method == "POST" else None
)
return fees
@cached_property
def positions(self):
positions = list(self.order.positions.select_related(
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
'seat', 'subevent',
).prefetch_related('granted_memberships', 'issued_gift_cards'))
'seat',
).prefetch_related(
Prefetch(
'subevent',
queryset=self.request.event.subevents.all(),
),
'granted_memberships',
'issued_gift_cards',
'addons',
).annotate(
_seat_allowed=Exists(SeatCategoryMapping.objects.filter(subevent=OuterRef("subevent"), product=OuterRef("item")))
))
for p in positions:
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
initial={'seat': p.seat.seat_guid if p.seat else None},
data=self.request.POST if self.request.method == "POST" else None)
p.form = OrderPositionChangeForm(
prefix='op-{}'.format(p.pk),
instance=p,
items=self.items,
initial={'seat': p.seat.seat_guid if p.seat else None},
data=self.request.POST if self.request.method == "POST" else None
)
return positions
def get_context_data(self, **kwargs):
+2 -1
View File
@@ -134,7 +134,7 @@ from pretix.control.permissions import (
organizer_permission_required,
)
from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
@@ -2664,6 +2664,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html'
permission = 'organizer.settings.general:write'
model = LogEntry
paginator_class = LargeResultSetPaginator
context_object_name = 'logs'
def get_queryset(self):
+15
View File
@@ -46,6 +46,7 @@ from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
@@ -95,6 +96,20 @@ class RecentAuthenticationRequiredMixin:
def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
# the XMLHttpRequest is unable to detect redirects.
return HttpResponse(
"Authentication required",
status=401,
headers={
# Appending ?next= is handled by client, because it should be the top-level context url,
# not the URL called in the background
"X-Login-Url": reverse('control:user.reauth')
}
)
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
return super().dispatch(request, *args, **kwargs)
+3
View File
@@ -53,6 +53,7 @@ from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -350,6 +351,8 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
return super().post(request, *args, **kwargs)
def get_success_url(self) -> str:
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
return self.request.GET.get("next")
return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
+3 -3
View File
@@ -28,7 +28,7 @@ from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.timezone import localtime, now, override
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
from geoip2.errors import AddressNotFoundError
@@ -167,13 +167,13 @@ def handle_login_source(user, request):
})
if user.known_login_sources.count() > 1:
# Do not send on first login or first login after introduction of this feature:
with language(user.locale):
with language(user.locale), override(user.timezone):
mail(
user.email,
_('New sign-in to your account'),
'pretixcontrol/email/login_notice.txt',
{
'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
'when': date_format(localtime(src.last_seen), 'DATETIME_FORMAT'),
'agent': src.agent_type,
'os': src.os_type,
# ua-parser returns "Other" for unidentified desktop devices.
+32 -54
View File
@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-06-20 17:00+0000\n"
"PO-Revision-Date: 2026-07-27 17:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
"da/>\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.6.1\n"
"X-Generator: Weblate 2026.7.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
#: pretix/control/templates/pretixcontrol/events/index.html
@@ -177,7 +177,7 @@ msgstr "Spansk (Latinamerika)"
#: pretix/_base_settings.py
msgid "Thai"
msgstr ""
msgstr "Thai"
#: pretix/_base_settings.py
msgid "Turkish"
@@ -436,7 +436,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format
msgid "You've been invited to join %(organizer)s"
msgstr ""
msgstr "Du er blevet inviteret til at tilslutte dig %(organizer)s"
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team."
@@ -458,9 +458,8 @@ msgid "Medium connected to other event"
msgstr "Medie er tilknyttet et andet arrangement"
#: pretix/api/views/checkin.py
#, fuzzy
msgid "You cannot exchange a medium for a medium."
msgstr "Du kan ikke annullere denne bestilling."
msgstr "Du kan ikke bytte en medium mod en anden medium."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format
@@ -4056,43 +4055,32 @@ msgid "Users"
msgstr "Brugere"
#: pretix/base/models/auth.py
#, fuzzy
#| msgid "Changes to your account at {organizer}"
msgid "Changes to your account"
msgstr "Ændringer på din konto hos {organizer}"
msgstr "Ændringer på din konto"
#: pretix/base/models/auth.py
#, fuzzy, python-brace-format
#| msgid ""
#| "to confirm changing your email address from {old_email}\n"
#| "to {new_email}, use the following code:"
#, python-brace-format
msgid ""
"To change your email address from {old_email} to {new_email}, use the "
"following code:"
msgstr ""
"For at bekræfte ændringen af din e-mailadresse fra {old_email}\n"
"til {new_email} skal du bruge følgende kode:"
"Hvis du vil ændre din e-mailadresse fra {old_email} til {new_email}, skal du "
"bruge følgende kode:"
#: pretix/base/models/auth.py
#, fuzzy, python-brace-format
#| msgid ""
#| "to confirm changing your email address from {old_email}\n"
#| "to {new_email}, use the following code:"
#, python-brace-format
msgid ""
"To verify your email address {email} on {instance}, use the following code:"
msgstr ""
"For at bekræfte ændringen af din e-mailadresse fra {old_email}\n"
"til {new_email} skal du bruge følgende kode:"
"Brug følgende kode til at bekræfte din e-mailadresse {email} på {instance}:"
#: pretix/base/models/auth.py
#, fuzzy
msgid "Your confirmation code"
msgstr "Bekræftelsestekst"
msgstr "Din bekræftelseskode"
#: pretix/base/models/auth.py
#, fuzzy
msgid "Reset your password"
msgstr "Nulstil adgangskode"
msgstr "Nulstil din adgangskode"
#: pretix/base/models/checkin.py
msgid "All products (including newly created ones)"
@@ -4253,22 +4241,16 @@ msgid "Check-in annulled"
msgstr "Check-in annulleret"
#: pretix/base/models/checkin.py
#, fuzzy
#| msgid "Ticket already used"
msgid "Ticket already exchanged"
msgstr "Billet allerede i brug"
msgstr "Billetten er allerede ombyttet"
#: pretix/base/models/checkin.py
#, fuzzy
#| msgid "Reusable media"
msgid "Reusable medium invalid"
msgstr "Genanvendelige medier"
msgstr "Genanvendeligt medium er ugyldigt"
#: pretix/base/models/checkin.py
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists"
msgstr "Genanvendelig medietype"
msgstr "Det genanvendelige medium findes allerede"
#: pretix/base/models/customers.py
msgid "Provider name"
@@ -4843,10 +4825,8 @@ msgstr ""
"begge dele."
#: pretix/base/models/event.py
#, fuzzy
#| msgid "The bundled item must belong to the same event as the item."
msgid "Property and event must belong to the same organizer."
msgstr "Artikelpakken skal tilhøre samme arrangement som selve artiklen."
msgstr "Ejendommen og arrangementet skal tilhøre den samme arrangør."
#: pretix/base/models/event.py pretix/base/models/organizer.py
msgid "Link text"
@@ -5082,42 +5062,40 @@ msgid "Show product with info on why its unavailable"
msgstr "Vis produktet med oplysninger om, hvorfor det ikke er tilgængeligt"
#: pretix/base/models/items.py
#, fuzzy
#| msgid "Don't use re-usable media, use regular one-off tickets"
msgid "Don't use reusable media, use regular one-off tickets"
msgstr "Brug ikke genanvendelige medier, brug almindelige engangsbilletter"
msgstr "Brug ikke genanvendelige medier, men almindelige engangsbilletter"
#: pretix/base/models/items.py
msgid "Require a previously unknown medium to be newly added"
msgstr "Påkræv, at et førhen ukendt medie bliver tilføjet fornylig"
#: pretix/base/models/items.py
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, replacing any previous tickets"
msgstr "Påkræv genanvendelse af et eksisterende medie"
msgstr ""
"Påkræv, at et eksisterende medium genbruges, hvorved eventuelle tidligere "
"billetter erstattes"
#: pretix/base/models/items.py
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, replacing any "
"previous tickets"
msgstr "Påkræv, at der enten bruges et eksisterende eller et nyt medie"
msgstr ""
"Påkræv, at der anvendes enten et eksisterende eller et nyt medium, som "
"erstatter eventuelle tidligere billetter"
#: pretix/base/models/items.py
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, adding to any previous tickets"
msgstr "Påkræv genanvendelse af et eksisterende medie"
msgstr ""
"Påkræv, at et eksisterende medium genbruges og tilføjes samtlige tidligere "
"billetter"
#: pretix/base/models/items.py
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, adding to any "
"previous tickets"
msgstr "Påkræv, at der enten bruges et eksisterende eller et nyt medie"
msgstr ""
"Påkræv, at der anvendes enten et eksisterende eller et nyt medie, hvilket "
"tilføjes til eventuelle tidligere billetter"
#: pretix/base/models/items.py
msgid "Category"
+11 -18
View File
@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2024-09-23 18:00+0000\n"
"PO-Revision-Date: 2026-07-27 17:00+0000\n"
"Last-Translator: Albizuri <oier@puntu.eus>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/eu/"
">\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/"
"eu/>\n"
"Language: eu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7.2\n"
"X-Generator: Weblate 2026.7.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
#: pretix/control/templates/pretixcontrol/events/index.html
@@ -13559,10 +13559,8 @@ msgstr ""
#: pretix/base/templates/pretixbase/email/order_details.html
#: pretix/presale/templates/pretixpresale/event/base.html
#: pretix/presale/templates/pretixpresale/organizers/base.html
#, fuzzy
#| msgid "Contact:"
msgid "Contact"
msgstr "Kontaktua:"
msgstr "Harremanetarako"
#: pretix/base/templates/pretixbase/email/shred_completed.txt
#, fuzzy, python-format
@@ -18285,7 +18283,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
#: pretix/presale/templates/pretixpresale/organizers/index.html
msgid "Tickets"
msgstr ""
msgstr "Sarrerak"
#: pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/event/tax.html
@@ -33876,12 +33874,11 @@ msgid "Show full-size image of %(item)s"
msgstr "Erakutsi %(item)s-ren irudi osoa"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html
#, fuzzy, python-format
#| msgid "Upcoming events"
#, python-format
msgid "%(amount)s× in your cart"
msgid_plural "%(amount)s× in your cart"
msgstr[0] "Hurrengo ekitaldiak"
msgstr[1] "Hurrengo ekitaldiak"
msgstr[0] "%(amount)s× zure saskian"
msgstr[1] "%(amount)s× zure saskian"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html
#: pretix/presale/templates/pretixpresale/event/voucher.html
@@ -34125,18 +34122,14 @@ msgstr ""
"Kontuan izan oraindik zure ordainketaren zain gaudela prozesua osatzeko."
#: pretix/presale/templates/pretixpresale/event/order.html
#, fuzzy
#| msgid ""
#| "Please bookmark or save the link to this exact page if you want to access "
#| "your order later. We also sent you an email containing the link to the "
#| "address you specified."
msgid ""
"Please bookmark or save the link to this exact page if you want to access "
"your order later. We also sent you an email to the address you specified "
"containing the link to this page."
msgstr ""
"Mesedez, gorde orri honetarako esteka zure eskaera geroago ikusi nahi "
"baduzu. E-mail bat ere bidali dizugu, zehaztutako helbidera lotura duena."
"baduzu. E-mail bat ere bidali dizugu zehaztu duzun helbidera, orrialde "
"honetarako estekarekin."
#: pretix/presale/templates/pretixpresale/event/order.html
#, fuzzy
File diff suppressed because it is too large Load Diff
-1
View File
@@ -111,7 +111,6 @@ class Paypal(BasePaymentProvider):
('client_id',
forms.CharField(
label=_('Client ID'),
max_length=80,
min_length=80,
help_text=format_html(
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
@@ -15,8 +15,12 @@ $(function () {
if ($("#logs_target").length == 0) {
return;
}
$.get("logs/embed", function (data) {
$("#logs_target").html(data)
add_log_expand_handlers($("#logs_target"))
$.get("dashboard/partials/logs", function (data) {
$("#logs_target").html(data)
add_log_expand_handlers($("#logs_target"))
});
$.get("dashboard/partials/warnings", function (data) {
$("#warnings_loading").remove()
$("#warnings_target").html(data)
});
});
+16
View File
@@ -1164,6 +1164,22 @@ def test_event_update_seating(token_client, organizer, event, item, seatingplan)
assert all(seat['product'] == item.pk for seat in resp.data['results'])
@pytest.mark.django_db
def test_event_no_create_or_delete_seats(token_client, organizer, event, item, seatingplan):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/'.format(organizer.slug, event.slug),
{
'blocked': False,
},
format='json'
)
assert resp.status_code == 405
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/seats/3/'.format(organizer.slug, event.slug),
)
assert resp.status_code == 405
@pytest.mark.django_db
def test_event_update_seating_invalid_product(token_client, organizer, event, item, seatingplan):
resp = token_client.patch(
+2
View File
@@ -90,6 +90,7 @@ event_urls = [
"delete/",
"dangerzone/",
"cancel/",
"quickstart/",
"settings/",
"settings/plugins",
"settings/payment",
@@ -311,6 +312,7 @@ event_permission_urls = [
("event.settings.general:write", "live/", 200, HTTP_GET),
("event.settings.general:write", "delete/", 200, HTTP_GET),
("event.settings.general:write", "dangerzone/", 200, HTTP_GET),
("event.settings.general:write", "quickstart/", 200, HTTP_GET),
("event.settings.general:write", "settings/", 200, HTTP_GET),
# ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions
("event.settings.payment:write", "settings/payment", 200, HTTP_POST),