Compare commits

..

22 Commits

Author SHA1 Message Date
Martin Gross
39115152a6 Update src/pretix/base/customersso/oidc.py
Co-authored-by: Mira <weller@rami.io>
2025-01-30 13:39:22 +01:00
Martin Gross
7ce790dbb9 Actually fix indentation... 2025-01-30 13:02:49 +01:00
Martin Gross
9a4a0df625 isort 2025-01-30 12:39:46 +01:00
Martin Gross
a8376b9a79 Add missing imports; fix indentation 2025-01-30 12:38:12 +01:00
Martin Gross
b21041a833 Apply suggestions from code review
Co-authored-by: Mira <weller@rami.io>
2025-01-30 12:35:58 +01:00
Martin Gross
e8a716273e OIDC: Allow to add query parameters to Authorization UR 2025-01-29 11:55:12 +01:00
Richard Schreiber
59a7845ac4 [A11y] Improve focus handling for widget overlay
* move iframe after close-button to follow tab-order

* add missing prevActiveElement

* prepare focus-handling for error_message

* iframe.src through prop instead of directly accessing it

* do not change close button HTML-element for compatability

* make all overlay elements role=dialog and modals

* fix close button

* fix re-opening of iframe

* make error-message read out when shown

* Improve handling of frame_src with frame_loading

* manually focus continue or close button in alert-box

* fix btn-focus in transition

* Improve quantity group
2025-01-28 11:04:38 +01:00
Cornelius Kibelka
ef1024d231 Translations: Update Portuguese (Brazil)
Currently translated at 14.8% (870 of 5844 strings)

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

powered by weblate
2025-01-28 10:13:32 +01:00
Cornelius Kibelka
71d0d72425 Translations: Update Portuguese (Brazil)
Currently translated at 14.1% (825 of 5844 strings)

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

powered by weblate
2025-01-27 17:40:37 +01:00
Hijiri Umemoto
6252e526bf Translations: Update Japanese
Currently translated at 100.0% (5844 of 5844 strings)

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

powered by weblate
2025-01-27 17:40:37 +01:00
Rosariocastellana
f3d8a1c2e1 Translations: Update Italian
Currently translated at 24.2% (1419 of 5844 strings)

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

powered by weblate
2025-01-27 17:40:37 +01:00
Martin Gross
148a3b2933 MetricsMiddleware: Do not record pretix_view_duration_seconds for urls with no url.url_name (#4784) 2025-01-27 16:46:58 +01:00
Mira
709633d6fb Register missing LogEntryTypes (#4779) 2025-01-27 15:35:29 +01:00
Mira
af3418db54 Fix company & vat_id dependencies on is_business (#4777) 2025-01-27 11:55:39 +01:00
Mira
ca8d253114 Bugfixes for LogEntryTypes refactoring (#4778) 2025-01-24 16:19:55 +01:00
Mira Weller
f014a9bbd3 Reapply "Implement hidden_if_item_available_mode option (Z#23177008) (#4776)"
This reverts commit 5cd7959e86.
2025-01-24 14:48:28 +01:00
Mira Weller
3e5bfb44d2 Revert "Preliminary migration"
This reverts commit 1736efbdc3.
2025-01-24 14:48:28 +01:00
Mira Weller
1736efbdc3 Preliminary migration 2025-01-24 14:08:13 +01:00
Mira Weller
5cd7959e86 Revert "Implement hidden_if_item_available_mode option (Z#23177008) (#4776)"
This reverts commit b847612e1a.
2025-01-24 14:07:55 +01:00
Mira
b847612e1a Implement hidden_if_item_available_mode option (Z#23177008) (#4776) 2025-01-24 11:24:50 +01:00
Mira
832f4e4d68 Define LogEntryTypes for all actions in pretix core, improve content_object handling (#4768)
Create LogEntryType definitions for all missing action_types (order changes, check-in events, settings changes of PaymentProviders and TicketOutputs).

Check whether the stored content_object is of the expected model type, preventing incorrect links.

Refactoring:
-    Move the base LogEntryType definitions for our models to their own file
-    Move HTML escaping into make_link to make it less likely to oversee in the LogEntryType definitions
-    Log pretix.event.order.deleted with the deleted Order model as content_object, matching the other *.deleted action_types
2025-01-24 10:05:19 +01:00
Mira
0a23aeece4 Allow 0% tax rate on event creation (#4756)
(but still warn if tax rate is not filled at all)
2025-01-23 12:59:39 +01:00
27 changed files with 815 additions and 615 deletions

View File

@@ -69,6 +69,10 @@ hidden_if_available integer **DEPRECATED*
hidden_if_item_available integer The internal ID of a different item, or ``null``. If hidden_if_item_available integer The internal ID of a different item, or ``null``. If
set, this item won't be shown publicly as long as this set, this item won't be shown publicly as long as this
other item is available. other item is available.
hidden_if_item_available_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``hidden_if_item_available`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
require_voucher boolean If ``true``, this item can only be bought using a require_voucher boolean If ``true``, this item can only be bought using a
voucher that is specifically assigned to this item. voucher that is specifically assigned to this item.
hide_without_voucher boolean If ``true``, this item is only shown during the voucher hide_without_voucher boolean If ``true``, this item is only shown during the voucher
@@ -239,6 +243,10 @@ meta_data object Values set fo
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
deprecated. deprecated.
.. versionchanged:: 2025.01
The ``hidden_if_item_available_mode`` attributes has been added.
Notes Notes
----- -----
@@ -308,6 +316,7 @@ Endpoints
"available_until_mode": "hide", "available_until_mode": "hide",
"hidden_if_available": null, "hidden_if_available": null,
"hidden_if_item_available": null, "hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false, "require_voucher": false,
"hide_without_voucher": false, "hide_without_voucher": false,
"allow_cancel": true, "allow_cancel": true,
@@ -459,6 +468,7 @@ Endpoints
"available_until_mode": "hide", "available_until_mode": "hide",
"hidden_if_available": null, "hidden_if_available": null,
"hidden_if_item_available": null, "hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false, "require_voucher": false,
"hide_without_voucher": false, "hide_without_voucher": false,
"allow_cancel": true, "allow_cancel": true,
@@ -589,6 +599,7 @@ Endpoints
"available_until_mode": "hide", "available_until_mode": "hide",
"hidden_if_available": null, "hidden_if_available": null,
"hidden_if_item_available": null, "hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false, "require_voucher": false,
"hide_without_voucher": false, "hide_without_voucher": false,
"allow_cancel": true, "allow_cancel": true,
@@ -705,6 +716,7 @@ Endpoints
"available_until_mode": "hide", "available_until_mode": "hide",
"hidden_if_available": null, "hidden_if_available": null,
"hidden_if_item_available": null, "hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false, "require_voucher": false,
"hide_without_voucher": false, "hide_without_voucher": false,
"allow_cancel": true, "allow_cancel": true,
@@ -855,6 +867,7 @@ Endpoints
"available_until_mode": "hide", "available_until_mode": "hide",
"hidden_if_available": null, "hidden_if_available": null,
"hidden_if_item_available": null, "hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false, "require_voucher": false,
"hide_without_voucher": false, "hide_without_voucher": false,
"generate_tickets": null, "generate_tickets": null,

View File

@@ -272,7 +272,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling', 'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations', 'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets', 'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist', 'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
'issue_giftcard', 'meta_data', 'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type', 'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days', 'grant_membership_duration_like_event', 'grant_membership_duration_days',

View File

@@ -24,7 +24,7 @@ import hashlib
import logging import logging
import time import time
from datetime import datetime from datetime import datetime
from urllib.parse import urlencode, urljoin from urllib.parse import parse_qsl, urlencode, urljoin
import jwt import jwt
import requests import requests
@@ -139,6 +139,11 @@ def oidc_validate_and_complete_config(config):
) )
) )
if "query_parameters" in config and config["query_parameters"]:
config["query_parameters"] = urlencode(
parse_qsl(config["query_parameters"])
)
config['provider_config'] = provider_config config['provider_config'] = provider_config
return config return config
@@ -154,6 +159,10 @@ def oidc_authorize_url(provider, state, redirect_uri):
'state': state, 'state': state,
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
} }
if "query_parameters" in provider.configuration and provider.configuration["query_parameters"]:
params.update(parse_qsl(provider.configuration["query_parameters"]))
return endpoint + '?' + urlencode(params) return endpoint + '?' + urlencode(params)

View File

@@ -1025,10 +1025,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'autocomplete': 'address-level2', 'autocomplete': 'address-level2',
}), }),
'company': forms.TextInput(attrs={ 'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization', 'autocomplete': 'organization',
}), }),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}), 'vat_id': forms.TextInput(),
'internal_reference': forms.TextInput, 'internal_reference': forms.TextInput,
} }
labels = { labels = {
@@ -1059,10 +1058,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# If an individual or company address is acceptable, #id_is_business_0 == individual, _1 == company. self.fields["company"].widget.attrs["data-display-dependency"] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
# However, if only company addresses are acceptable, #id_is_business_0 == company and is the only choice self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_{int(not self.company_required)}'
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_{int(not self.company_required)}'
if not self.ask_vat_id: if not self.ask_vat_id:
del self.fields['vat_id'] del self.fields['vat_id']
@@ -1143,9 +1140,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
) )
if self.address_required and not self.company_required and not self.all_optional: if self.address_required and not self.company_required and not self.all_optional:
if not event.settings.invoice_name_required: if not event.settings.invoice_name_required:
self.fields['name_parts'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_0' self.fields['name_parts'].widget.attrs['data-required-if'] = f'input[name="{self.add_prefix("is_business")}"][value="individual"]'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1' self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1' self.fields['company'].widget.attrs['data-required-if'] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
if not event.settings.invoice_address_beneficiary: if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary'] del self.fields['beneficiary']

View File

@@ -0,0 +1,165 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from typing import Optional
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from pretix.base.signals import EventPluginRegistry
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
if a_map:
if 'href' not in a_map:
a_map['val'] = format_html('<i>{val}</i>', **a_map)
elif is_active:
a_map['val'] = format_html('<a href="{href}">{val}</a>', **a_map)
elif event and plugin_name:
a_map['val'] = format_html(
'<i>{val}</i> <a href="{plugin_href}">'
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>',
**a_map,
errmes=_("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
plugin_href=reverse('control:event.settings.plugins', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '#plugin_' + plugin_name,
)
else:
a_map['val'] = format_html(
'<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>',
**a_map,
errmes=_("The relevant plugin is currently not active."),
)
return format_html(wrapper, **a_map)
class LogEntryTypeRegistry(EventPluginRegistry):
def __init__(self):
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
def register(self, *objs):
for obj in objs:
if not isinstance(obj, LogEntryType):
raise TypeError('Entries must be derived from LogEntryType')
if obj.__module__.startswith('pretix.base.'):
raise TypeError('Must not register base classes, only derived ones')
return super().register(*objs)
def new_from_dict(self, data):
"""
Register multiple instance of a `LogEntryType` class with different `action_type`
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return clz
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry()
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
def __init__(self, action_type=None, plain=None):
if action_type:
self.action_type = action_type
if plain:
self.plain = plain
def display(self, logentry, data):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
data = defaultdict(lambda: '?', data)
return plain.format_map(data)
else:
return plain
def get_object_link_info(self, logentry) -> Optional[dict]:
"""
Return information to generate a link to the `content_object` of a given log entry.
Not implemented in the base class, causing the object link to be omitted.
:return: Dictionary with the keys ``href`` (URL to view/edit the object) and
``val`` (text for the anchor element)
"""
pass
def get_object_link(self, logentry):
a_map = self.get_object_link_info(logentry)
return make_link(a_map, self.object_link_wrapper)
object_link_wrapper = '{val}'
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None

View File

@@ -19,137 +19,20 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from collections import defaultdict from typing import Optional
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import EventPluginRegistry from pretix.base.models import (
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
Voucher,
)
from .logentrytype_registry import ( # noqa
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None): ClearDataShredderMixin, LogEntryType, NoOpShredderMixin, log_entry_types,
if a_map: make_link, LogEntryTypeRegistry,
if is_active: )
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
elif event and plugin_name:
a_map['val'] = (
'<i>{val}</i> <a href="{plugin_href}">'
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>'
).format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
"plugin_href": reverse('control:event.settings.plugins', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '#plugin_' + plugin_name,
})
else:
a_map['val'] = '<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>'.format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active."),
})
return wrapper.format_map(a_map)
class LogEntryTypeRegistry(EventPluginRegistry):
def __init__(self):
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
def register(self, *objs):
for obj in objs:
if not isinstance(obj, LogEntryType):
raise TypeError('Entries must be derived from LogEntryType')
if obj.__module__ == LogEntryType.__module__:
raise TypeError('Must not register base classes, only derived ones')
return super().register(*objs)
def new_from_dict(self, data):
"""
Register multiple instance of a `LogEntryType` class with different `action_type`
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return clz
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry()
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
def __init__(self, action_type=None, plain=None):
if action_type:
self.action_type = action_type
if plain:
self.plain = plain
def display(self, logentry):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
data = defaultdict(lambda: '?', logentry.parsed_data)
return plain.format_map(data)
else:
return plain
def get_object_link_info(self, logentry) -> dict:
"""
Return information to generate a link to the `content_object` of a given log entry.
Not implemented in the base class, causing the object link to be omitted.
:return: Dictionary with the keys ``href`` (containing a URL to view/edit the object) and ``val`` (containing the
escaped text for the anchor element)
"""
pass
def get_object_link(self, logentry):
a_map = self.get_object_link_info(logentry)
return make_link(a_map, self.object_link_wrapper)
object_link_wrapper = '{val}'
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
class EventLogEntryType(LogEntryType): class EventLogEntryType(LogEntryType):
@@ -157,15 +40,27 @@ class EventLogEntryType(LogEntryType):
Base class for any `LogEntry` type whose `content_object` is either an `Event` itself or belongs to a specific `Event`. Base class for any `LogEntry` type whose `content_object` is either an `Event` itself or belongs to a specific `Event`.
""" """
def get_object_link_info(self, logentry) -> dict: def get_object_link_info(self, logentry) -> Optional[dict]:
if hasattr(self, 'object_link_viewname') and logentry.content_object: if hasattr(self, 'object_link_viewname'):
content = logentry.content_object
if not content:
if logentry.content_type_id:
return {
'val': _('(deleted)'),
}
else:
return
if hasattr(self, 'content_type') and not isinstance(content, self.content_type):
return
return { return {
'href': reverse(self.object_link_viewname, kwargs={ 'href': reverse(self.object_link_viewname, kwargs={
'event': logentry.event.slug, 'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug, 'organizer': logentry.event.organizer.slug,
**self.object_link_args(logentry.content_object), **self.object_link_args(content),
}), }),
'val': escape(self.object_link_display_name(logentry.content_object)), 'val': self.object_link_display_name(logentry.content_object),
} }
def object_link_args(self, content_object): def object_link_args(self, content_object):
@@ -182,6 +77,7 @@ class EventLogEntryType(LogEntryType):
class OrderLogEntryType(EventLogEntryType): class OrderLogEntryType(EventLogEntryType):
object_link_wrapper = _('Order {val}') object_link_wrapper = _('Order {val}')
object_link_viewname = 'control:event.order' object_link_viewname = 'control:event.order'
content_type = Order
def object_link_args(self, order): def object_link_args(self, order):
return {'code': order.code} return {'code': order.code}
@@ -194,6 +90,7 @@ class VoucherLogEntryType(EventLogEntryType):
object_link_wrapper = _('Voucher {val}') object_link_wrapper = _('Voucher {val}')
object_link_viewname = 'control:event.voucher' object_link_viewname = 'control:event.voucher'
object_link_argname = 'voucher' object_link_argname = 'voucher'
content_type = Voucher
def object_link_display_name(self, voucher): def object_link_display_name(self, voucher):
if len(voucher.code) > 6: if len(voucher.code) > 6:
@@ -205,49 +102,46 @@ class ItemLogEntryType(EventLogEntryType):
object_link_wrapper = _('Product {val}') object_link_wrapper = _('Product {val}')
object_link_viewname = 'control:event.item' object_link_viewname = 'control:event.item'
object_link_argname = 'item' object_link_argname = 'item'
content_type = Item
class SubEventLogEntryType(EventLogEntryType): class SubEventLogEntryType(EventLogEntryType):
object_link_wrapper = pgettext_lazy('subevent', 'Date {val}') object_link_wrapper = pgettext_lazy('subevent', 'Date {val}')
object_link_viewname = 'control:event.subevent' object_link_viewname = 'control:event.subevent'
object_link_argname = 'subevent' object_link_argname = 'subevent'
content_type = SubEvent
class QuotaLogEntryType(EventLogEntryType): class QuotaLogEntryType(EventLogEntryType):
object_link_wrapper = _('Quota {val}') object_link_wrapper = _('Quota {val}')
object_link_viewname = 'control:event.items.quotas.show' object_link_viewname = 'control:event.items.quotas.show'
object_link_argname = 'quota' object_link_argname = 'quota'
content_type = Quota
class DiscountLogEntryType(EventLogEntryType): class DiscountLogEntryType(EventLogEntryType):
object_link_wrapper = _('Discount {val}') object_link_wrapper = _('Discount {val}')
object_link_viewname = 'control:event.items.discounts.edit' object_link_viewname = 'control:event.items.discounts.edit'
object_link_argname = 'discount' object_link_argname = 'discount'
content_type = Discount
class ItemCategoryLogEntryType(EventLogEntryType): class ItemCategoryLogEntryType(EventLogEntryType):
object_link_wrapper = _('Category {val}') object_link_wrapper = _('Category {val}')
object_link_viewname = 'control:event.items.categories.edit' object_link_viewname = 'control:event.items.categories.edit'
object_link_argname = 'category' object_link_argname = 'category'
content_type = ItemCategory
class QuestionLogEntryType(EventLogEntryType): class QuestionLogEntryType(EventLogEntryType):
object_link_wrapper = _('Question {val}') object_link_wrapper = _('Question {val}')
object_link_viewname = 'control:event.items.questions.show' object_link_viewname = 'control:event.items.questions.show'
object_link_argname = 'question' object_link_argname = 'question'
content_type = Question
class TaxRuleLogEntryType(EventLogEntryType): class TaxRuleLogEntryType(EventLogEntryType):
object_link_wrapper = _('Tax rule {val}') object_link_wrapper = _('Tax rule {val}')
object_link_viewname = 'control:event.settings.tax.edit' object_link_viewname = 'control:event.settings.tax.edit'
object_link_argname = 'rule' object_link_argname = 'rule'
content_type = TaxRule
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-01-23 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0275_alter_question_valid_number_max_and_more"),
]
operations = [
migrations.AddField(
model_name="item",
name="hidden_if_item_available_mode",
field=models.CharField(default="hide", max_length=16),
),
]

View File

@@ -442,8 +442,12 @@ class Item(LoggedModel):
UNAVAIL_MODE_INFO = "info" UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = ( UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")), (UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")), (UNAVAIL_MODE_INFO, _("Show product with info on why its unavailable")),
) )
UNAVAIL_MODE_ICONS = {
UNAVAIL_MODE_HIDDEN: 'eye-slash',
UNAVAIL_MODE_INFO: 'info'
}
MEDIA_POLICY_REUSE = 'reuse' MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new' MEDIA_POLICY_NEW = 'new'
@@ -596,6 +600,11 @@ class Item(LoggedModel):
"be a short period in which both products are visible while all tickets of the referenced " "be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.") "product are reserved, but not yet sold.")
) )
hidden_if_item_available_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
require_voucher = models.BooleanField( require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'), verbose_name=_('This product can only be bought using a voucher.'),
default=False, default=False,
@@ -885,6 +894,8 @@ class Item(LoggedModel):
return 'available_from' return 'available_from'
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt: elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
return 'available_until' return 'available_until'
elif self.hidden_if_item_available and self._dependency_available:
return 'hidden_if_item_available'
else: else:
return None return None

View File

@@ -40,7 +40,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pretix.base.logentrytypes import log_entry_types, make_link from pretix.base.logentrytype_registry import log_entry_types, make_link
from pretix.base.signals import is_app_active, logentry_object_link from pretix.base.signals import is_app_active, logentry_object_link
@@ -93,7 +93,7 @@ class LogEntry(models.Model):
def display(self): def display(self):
log_entry_type, meta = log_entry_types.get(action_type=self.action_type) log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type: if log_entry_type:
return log_entry_type.display(self) return log_entry_type.display(self, self.parsed_data)
from ..signals import logentry_display from ..signals import logentry_display

View File

@@ -355,7 +355,7 @@ class Order(LockModel, LoggedModel):
if not self.testmode: if not self.testmode:
raise TypeError("Only test mode orders can be deleted.") raise TypeError("Only test mode orders can be deleted.")
self.event.log_action( self.log_action(
'pretix.event.order.deleted', user=user, auth=auth, 'pretix.event.order.deleted', user=user, auth=auth,
data={ data={
'code': self.code, 'code': self.code,

View File

@@ -231,7 +231,7 @@ class EventWizardBasicsForm(I18nModelForm):
raise ValidationError({ raise ValidationError({
'timezone': _('Your default locale must be specified.') 'timezone': _('Your default locale must be specified.')
}) })
if not data.get("no_taxes") and not data.get("tax_rate"): if not data.get("no_taxes") and data.get("tax_rate") is None:
raise ValidationError({ raise ValidationError({
'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please ' 'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please '
'check "{field}" above.').format(field=self.fields["no_taxes"].label) 'check "{field}" above.').format(field=self.fields["no_taxes"].label)

View File

@@ -476,6 +476,7 @@ class ItemCreateForm(I18nModelForm):
'show_quota_left', 'show_quota_left',
'hidden_if_available', 'hidden_if_available',
'hidden_if_item_available', 'hidden_if_item_available',
'hidden_if_item_available_mode',
'require_bundling', 'require_bundling',
'require_membership', 'require_membership',
'grant_membership_type', 'grant_membership_type',
@@ -646,18 +647,12 @@ class ItemUpdateForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect( self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices, choices=self.fields['available_from_mode'].choices,
option_icons={ option_icons=Item.UNAVAIL_MODE_ICONS
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
) )
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect( self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices, choices=self.fields['available_until_mode'].choices,
option_icons={ option_icons=Item.UNAVAIL_MODE_ICONS
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
) )
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect( self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
@@ -672,6 +667,11 @@ class ItemUpdateForm(I18nModelForm):
attrs={'data-checkbox-dependency': '#id_require_voucher'} attrs={'data-checkbox-dependency': '#id_require_voucher'}
) )
self.fields['hidden_if_item_available_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['hidden_if_item_available_mode'].choices,
option_icons=Item.UNAVAIL_MODE_ICONS
)
if self.instance.hidden_if_available_id: if self.instance.hidden_if_available_id:
self.fields['hidden_if_available'].queryset = self.event.quotas.all() self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].help_text = format_html( self.fields['hidden_if_available'].help_text = format_html(
@@ -853,6 +853,7 @@ class ItemUpdateForm(I18nModelForm):
'show_quota_left', 'show_quota_left',
'hidden_if_available', 'hidden_if_available',
'hidden_if_item_available', 'hidden_if_item_available',
'hidden_if_item_available_mode',
'issue_giftcard', 'issue_giftcard',
'require_membership', 'require_membership',
'require_membership_types', 'require_membership_types',
@@ -970,18 +971,12 @@ class ItemVariationForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect( self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices, choices=self.fields['available_from_mode'].choices,
option_icons={ option_icons=Item.UNAVAIL_MODE_ICONS
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
) )
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect( self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices, choices=self.fields['available_until_mode'].choices,
option_icons={ option_icons=Item.UNAVAIL_MODE_ICONS
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
) )
self.meta_fields = [] self.meta_fields = []

View File

@@ -1043,6 +1043,15 @@ class SSOProviderForm(I18nModelForm):
label=pgettext_lazy('sso_oidc', 'Phone field'), label=pgettext_lazy('sso_oidc', 'Phone field'),
required=False, required=False,
) )
config_oidc_query_parameters = forms.CharField(
label=pgettext_lazy('sso_oidc', 'Query parameters'),
help_text=pgettext_lazy('sso_oidc', 'Optional query parameters, that will be added to calls to '
'the authorization endpoint. Enter as: {example}'.format(
example='<code>param1=value1&amp;param2=value2</code>'
),
),
required=False,
)
class Meta: class Meta:
model = CustomerSSOProvider model = CustomerSSOProvider

View File

@@ -33,16 +33,16 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import json
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from typing import Optional
import bleach import bleach
import dateutil.parser import dateutil.parser
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -68,128 +68,212 @@ OVERVIEW_BANLIST = [
] ]
def _display_order_changed(event: Event, logentry: LogEntry): class OrderChangeLogEntryType(OrderLogEntryType):
data = json.loads(logentry.data) prefix = _('The order has been changed:')
text = _('The order has been changed:') def display(self, logentry, data):
if logentry.action_type == 'pretix.event.order.changed.item': return self.prefix + ' ' + self.display_prefixed(logentry.event, logentry, data)
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return super().display(logentry, data)
class OrderPositionChangeLogEntryType(OrderChangeLogEntryType):
prefix = _('The order has been changed:')
def display(self, logentry, data):
return super().display(logentry, {**data, 'posid': data.get('positionid', '?')})
@log_entry_types.new()
class OrderItemChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.item'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item'])) old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']: if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation'])) old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
new_item = str(event.items.get(pk=data['new_item'])) new_item = str(event.items.get(pk=data['new_item']))
if data['new_variation']: if data['new_variation']:
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation'])) new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
return text + ' ' + _('Position #{posid}: {old_item} ({old_price}) changed ' return _('Position #{posid}: {old_item} ({old_price}) changed to {new_item} ({new_price}).').format(
'to {new_item} ({new_price}).').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
old_item=old_item, new_item=new_item, old_item=old_item, new_item=new_item,
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency), new_price=money_filter(Decimal(data['new_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.membership':
return text + ' ' + _('Position #{posid}: Used membership changed.').format(
posid=data.get('positionid', '?'), @log_entry_types.new()
) class OrderMembershipChanged(OrderPositionChangeLogEntryType):
elif logentry.action_type == 'pretix.event.order.changed.seat': action_type = 'pretix.event.order.changed.membership'
return text + ' ' + _('Position #{posid}: Seat "{old_seat}" changed ' plain = _('Position #{posid}: Used membership changed.')
'to "{new_seat}".').format(
posid=data.get('positionid', '?'),
old_seat=data.get('old_seat'), new_seat=data.get('new_seat'), @log_entry_types.new()
) class OrderSeatChanged(OrderPositionChangeLogEntryType):
elif logentry.action_type == 'pretix.event.order.changed.subevent': action_type = 'pretix.event.order.changed.seat'
plain = _('Position #{posid}: Seat "{old_seat}" changed to "{new_seat}".')
@log_entry_types.new()
class OrderSubeventChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.subevent'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_se = str(event.subevents.get(pk=data['old_subevent'])) old_se = str(event.subevents.get(pk=data['old_subevent']))
new_se = str(event.subevents.get(pk=data['new_subevent'])) new_se = str(event.subevents.get(pk=data['new_subevent']))
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price}) changed ' return _('Position #{posid}: Event date "{old_event}" ({old_price}) changed '
'to "{new_event}" ({new_price}).').format( 'to "{new_event}" ({new_price}).').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
old_event=old_se, new_event=new_se, old_event=old_se, new_event=new_se,
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency), new_price=money_filter(Decimal(data['new_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.price':
return text + ' ' + _('Price of position #{posid} changed from {old_price} '
'to {new_price}.').format( @log_entry_types.new()
class OrderPriceChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.price'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('Price of position #{posid} changed from {old_price} to {new_price}.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency), new_price=money_filter(Decimal(data['new_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.tax_rule':
@log_entry_types.new()
class OrderTaxRuleChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.tax_rule'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
if 'positionid' in data: if 'positionid' in data:
return text + ' ' + _('Tax rule of position #{posid} changed from {old_rule} ' return _('Tax rule of position #{posid} changed from {old_rule} to {new_rule}.').format(
'to {new_rule}.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '', old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']), new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
) )
elif 'fee' in data: elif 'fee' in data:
return text + ' ' + _('Tax rule of fee #{fee} changed from {old_rule} ' return _('Tax rule of fee #{fee} changed from {old_rule} to {new_rule}.').format(
'to {new_rule}.').format(
fee=data.get('fee', '?'), fee=data.get('fee', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '', old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']), new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
) )
elif logentry.action_type == 'pretix.event.order.changed.addfee':
return text + ' ' + str(_('A fee has been added'))
elif logentry.action_type == 'pretix.event.order.changed.feevalue': @log_entry_types.new()
return text + ' ' + _('A fee was changed from {old_price} to {new_price}.').format( class OrderFeeAdded(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.addfee'
plain = _('A fee has been added')
@log_entry_types.new()
class OrderFeeChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.feevalue'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('A fee was changed from {old_price} to {new_price}.').format(
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency), new_price=money_filter(Decimal(data['new_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.cancelfee':
return text + ' ' + _('A fee of {old_price} was removed.').format(
@log_entry_types.new()
class OrderFeeRemoved(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.cancelfee'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('A fee of {old_price} was removed.').format(
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.cancel':
@log_entry_types.new()
class OrderCanceled(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.cancel'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item'])) old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']: if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation'])) old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) canceled.').format( return _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
old_item=old_item, old_item=old_item,
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.add':
@log_entry_types.new()
class OrderPositionAdded(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.add'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
item = str(event.items.get(pk=data['item'])) item = str(event.items.get(pk=data['item']))
if data['variation']: if data['variation']:
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation'])) item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
if data['addon_to']: if data['addon_to']:
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to']) addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
return text + ' ' + _('Position #{posid} created: {item} ({price}) as an add-on to ' return _('Position #{posid} created: {item} ({price}) as an add-on to position #{addon_to}.').format(
'position #{addon_to}.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
item=item, addon_to=addon_to.positionid, item=item, addon_to=addon_to.positionid,
price=money_filter(Decimal(data['price']), event.currency), price=money_filter(Decimal(data['price']), event.currency),
) )
else: else:
return text + ' ' + _('Position #{posid} created: {item} ({price}).').format( return _('Position #{posid} created: {item} ({price}).').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
item=item, item=item,
price=money_filter(Decimal(data['price']), event.currency), price=money_filter(Decimal(data['price']), event.currency),
) )
elif logentry.action_type == 'pretix.event.order.changed.secret':
return text + ' ' + _('A new secret has been generated for position #{posid}.').format(
posid=data.get('positionid', '?'), @log_entry_types.new()
) class OrderSecretChanged(OrderPositionChangeLogEntryType):
elif logentry.action_type == 'pretix.event.order.changed.valid_from': action_type = 'pretix.event.order.changed.secret'
return text + ' ' + _('The validity start date for position #{posid} has been changed to {value}.').format( plain = _('A new secret has been generated for position #{posid}.')
@log_entry_types.new()
class OrderValidFromChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.valid_from'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('The validity start date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get( value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get(
'new_value') else '' 'new_value') else ''
) )
elif logentry.action_type == 'pretix.event.order.changed.valid_until':
return text + ' ' + _('The validity end date for position #{posid} has been changed to {value}.').format(
@log_entry_types.new()
class OrderValidUntilChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.valid_until'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('The validity end date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get('new_value') else '' value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get('new_value') else ''
) )
elif logentry.action_type == 'pretix.event.order.changed.add_block':
return text + ' ' + _('A block has been added for position #{posid}.').format(
posid=data.get('positionid', '?'), @log_entry_types.new()
) class OrderChangedBlockAdded(OrderPositionChangeLogEntryType):
elif logentry.action_type == 'pretix.event.order.changed.remove_block': action_type = 'pretix.event.order.changed.add_block'
return text + ' ' + _('A block has been removed for position #{posid}.').format( plain = _('A block has been added for position #{posid}.')
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.split': @log_entry_types.new()
class OrderChangedBlockRemoved(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.remove_block'
plain = _('A block has been removed for position #{posid}.')
@log_entry_types.new()
class OrderChangedSplit(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.split'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
old_item = str(event.items.get(pk=data['old_item'])) old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']: if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation'])) old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
@@ -198,194 +282,148 @@ def _display_order_changed(event: Event, logentry: LogEntry):
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'code': data['new_order'] 'code': data['new_order']
}) })
return mark_safe(escape(text) + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format( return mark_safe(self.prefix + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
old_item=escape(old_item), old_item=escape(old_item),
posid=data.get('positionid', '?'), posid=data.get('positionid', '?'),
order='<a href="{}">{}</a>'.format(url, data['new_order']), order='<a href="{}">{}</a>'.format(url, data['new_order']),
old_price=money_filter(Decimal(data['old_price']), event.currency), old_price=money_filter(Decimal(data['old_price']), event.currency),
)) ))
elif logentry.action_type == 'pretix.event.order.changed.split_from':
@log_entry_types.new()
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( return _('This order has been created by splitting the order {order}').format(
order=data['original_order'], order=data['original_order'],
) )
def _display_checkin(event, logentry): @log_entry_types.new_from_dict({
data = logentry.parsed_data 'pretix.event.checkin.unknown': (
_('Unknown scan of code "{barcode}" at {datetime} for list "{list}", type "{type}".'),
_('Unknown scan of code "{barcode}" for list "{list}", type "{type}".'),
),
'pretix.event.checkin.revoked': (
_('Scan of revoked code "{barcode}" at {datetime} for list "{list}", type "{type}", was uploaded.'),
_('Scan of revoked code "{barcode}" for list "{list}", type "{type}", was uploaded.'),
),
'pretix.event.checkin.denied': (
_('Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", error code "{errorcode}".'),
_('Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".'),
),
'pretix.control.views.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
'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):
self.display_plain(self.plain, logentry, data)
show_dt = False def display_plain(self, plain, logentry: LogEntry, data):
if 'datetime' in data: if isinstance(plain, tuple):
dt = dateutil.parser.parse(data.get('datetime')) plain_with_dt, plain_without_dt = plain
show_dt = abs((logentry.datetime - dt).total_seconds()) > 5 or 'forced' in data else:
tz = event.timezone plain_with_dt, plain_without_dt = plain, plain
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data: data = defaultdict(lambda: '?', data)
try:
checkin_list = event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if logentry.action_type == 'pretix.event.checkin.unknown': event = logentry.event
if show_dt:
return _( if 'list' in data:
'Unknown scan of code "{barcode}" at {datetime} for list "{list}", type "{type}".' try:
).format( data['list'] = event.checkin_lists.get(pk=data.get('list')).name
posid=data.get('positionid'), except CheckinList.DoesNotExist:
type=data.get('type'), data['list'] = _("(unknown)")
barcode=data.get('barcode')[:16], else:
datetime=dt_formatted, data['list'] = _("(unknown)")
list=checkin_list
data['barcode'] = data.get('barcode', '')[:16]
data['posid'] = logentry.parsed_data.get('positionid', '?')
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")
return str(plain_with_dt).format_map(data)
else:
return str(plain_without_dt).format_map(data)
@log_entry_types.new('pretix.event.checkin')
class CheckinLogEntryType(CheckinErrorLogEntryType):
def display(self, logentry: LogEntry, data):
if data.get('type') == Checkin.TYPE_EXIT:
return self.display_plain((
_('Position #{posid} has been checked out at {datetime} for list "{list}".'),
_('Position #{posid} has been checked out for list "{list}".'),
), logentry, data)
elif data.get('first'):
return self.display_plain((
_('Position #{posid} has been checked in at {datetime} for list "{list}".'),
_('Position #{posid} has been checked in for list "{list}".'),
), logentry, data)
elif data.get('forced'):
return self.display_plain(
_('A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'),
logentry, data
) )
else: else:
return _( return self.display_plain(
'Unknown scan of code "{barcode}" for list "{list}", type "{type}".' _('Position #{posid} has been scanned and rejected because it has already been scanned before '
).format( 'on list "{list}".'),
posid=data.get('positionid'), logentry, data
type=data.get('type'),
barcode=data.get('barcode')[:16],
list=checkin_list
) )
if logentry.action_type == 'pretix.event.checkin.revoked':
if show_dt:
return _(
'Scan scan of revoked code "{barcode}" at {datetime} for list "{list}", type "{type}", was uploaded.'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
datetime=dt_formatted,
list=checkin_list
)
else:
return _(
'Scan of revoked code "{barcode}" for list "{list}", type "{type}", was uploaded.'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
list=checkin_list
)
if logentry.action_type == 'pretix.event.checkin.denied': @log_entry_types.new()
if show_dt: class OrderConsentLogEntryType(OrderLogEntryType):
return _( action_type = 'pretix.event.order.consent'
'Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", '
'error code "{errorcode}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
errorcode=data.get('errorcode'),
datetime=dt_formatted,
list=checkin_list
)
else:
return _(
'Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
errorcode=data.get('errorcode'),
list=checkin_list
)
if data.get('type') == Checkin.TYPE_EXIT: def display(self, logentry: LogEntry, data):
if show_dt: return _('The user confirmed the following message: "{}"').format(
return _('Position #{posid} has been checked out at {datetime} for list "{list}".').format( bleach.clean(data.get('msg'), tags=set(), strip=True)
posid=data.get('positionid'), )
datetime=dt_formatted,
list=checkin_list
) @log_entry_types.new()
class OrderCanceledLogEntryType(OrderLogEntryType):
action_type = 'pretix.event.order.canceled'
def display(self, logentry: LogEntry, data):
comment = data.get('comment')
if comment:
return _('The order has been canceled (comment: "{comment}").').format(comment=comment)
else: else:
return _('Position #{posid} has been checked out for list "{list}".').format( return _('The order has been canceled.')
posid=data.get('positionid'),
list=checkin_list
) @log_entry_types.new()
if data.get('first'): class OrderPrintLogEntryType(OrderLogEntryType):
if show_dt: action_type = 'pretix.event.order.print'
return _('Position #{posid} has been checked in at {datetime} for list "{list}".').format(
posid=data.get('positionid'), def display(self, logentry: LogEntry, data):
datetime=dt_formatted, return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
list=checkin_list posid=data.get('positionid'),
) datetime=date_format(
else: dateutil.parser.parse(data["datetime"]).astimezone(logentry.event.timezone),
return _('Position #{posid} has been checked in for list "{list}".').format( "SHORT_DATETIME_FORMAT"
posid=data.get('positionid'), ),
list=checkin_list type=dict(PrintLog.PRINT_TYPES)[data["type"]],
)
else:
if data.get('forced'):
return _(
'A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'.format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
)
return _(
'Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'.format(
posid=data.get('positionid'),
list=checkin_list
)
) )
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display") @receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type.startswith('pretix.event.order.changed'):
return _display_order_changed(sender, logentry)
if logentry.action_type.startswith('pretix.event.payment.provider.'): if logentry.action_type.startswith('pretix.event.payment.provider.'):
return _('The settings of a payment provider have been changed.') return _('The settings of a payment provider have been changed.')
if logentry.action_type.startswith('pretix.event.tickets.provider.'): if logentry.action_type.startswith('pretix.event.tickets.provider.'):
return _('The settings of a ticket output provider have been changed.') return _('The settings of a ticket output provider have been changed.')
if logentry.action_type == 'pretix.event.order.consent':
return _('The user confirmed the following message: "{}"').format(
bleach.clean(logentry.parsed_data.get('msg'), tags=set(), strip=True)
)
if logentry.action_type == 'pretix.event.order.canceled':
comment = logentry.parsed_data.get('comment')
if comment:
return _('The order has been canceled (comment: "{comment}").').format(comment=comment)
else:
return _('The order has been canceled.')
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
if 'list' in logentry.parsed_data:
try:
checkin_list = sender.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
return _('The check-in of position #{posid} on list "{list}" has been reverted.').format(
posid=logentry.parsed_data.get('positionid'),
list=checkin_list,
)
if sender and logentry.action_type.startswith('pretix.event.checkin'):
return _display_checkin(sender, logentry)
if logentry.action_type == 'pretix.event.order.print':
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=logentry.parsed_data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(logentry.parsed_data["datetime"]).astimezone(sender.timezone),
"SHORT_DATETIME_FORMAT"
),
type=dict(PrintLog.PRINT_TYPES)[logentry.parsed_data["type"]],
)
@receiver(signal=orderposition_blocked_display, dispatch_uid="pretixcontrol_orderposition_blocked_display") @receiver(signal=orderposition_blocked_display, dispatch_uid="pretixcontrol_orderposition_blocked_display")
def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, block_name, **kwargs): def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, block_name, **kwargs):
@@ -396,6 +434,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.order.modified': _('The order details have been changed.'), 'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'), 'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'), 'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
@@ -486,17 +525,16 @@ class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed' action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.') plain = _('The voucher has been redeemed in order {order_code}.')
def display(self, logentry): def display(self, logentry, data):
data = json.loads(logentry.data)
data = defaultdict(lambda: '?', data)
url = reverse('control:event.order', kwargs={ url = reverse('control:event.order', kwargs={
'event': logentry.event.slug, 'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug, 'organizer': logentry.event.organizer.slug,
'code': data['order_code'] 'code': data.get('order_code', '?')
}) })
return mark_safe(self.plain.format( return format_html(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']), self.plain,
)) order_code=format_html('<a href="{}">{}</a>', url, data.get('order_code', '?')),
)
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
@@ -519,8 +557,8 @@ class CoreTaxRuleLogEntryType(TaxRuleLogEntryType):
class TeamMembershipLogEntryType(LogEntryType): class TeamMembershipLogEntryType(LogEntryType):
def display(self, logentry): def display(self, logentry, data):
return self.plain.format(user=logentry.parsed_data.get('email')) return self.plain.format(user=data.get('email'))
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
@@ -537,9 +575,9 @@ class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
class TeamMemberJoinedLogEntryType(LogEntryType): class TeamMemberJoinedLogEntryType(LogEntryType):
action_type = 'pretix.team.member.joined' action_type = 'pretix.team.member.joined'
def display(self, logentry): def display(self, logentry, data):
return _('{user} has joined the team using the invite sent to {email}.').format( return _('{user} has joined the team using the invite sent to {email}.').format(
user=logentry.parsed_data.get('email'), email=logentry.parsed_data.get('invite_email') user=data.get('email'), email=data.get('invite_email')
) )
@@ -547,23 +585,23 @@ class TeamMemberJoinedLogEntryType(LogEntryType):
class UserSettingsChangedLogEntryType(LogEntryType): class UserSettingsChangedLogEntryType(LogEntryType):
action_type = 'pretix.user.settings.changed' action_type = 'pretix.user.settings.changed'
def display(self, logentry): def display(self, logentry, data):
text = str(_('Your account settings have been changed.')) text = str(_('Your account settings have been changed.'))
if 'email' in logentry.parsed_data: if 'email' in data:
text = text + ' ' + str( text = text + ' ' + str(
_('Your email address has been changed to {email}.').format(email=logentry.parsed_data['email'])) _('Your email address has been changed to {email}.').format(email=data['email']))
if 'new_pw' in logentry.parsed_data: if 'new_pw' in data:
text = text + ' ' + str(_('Your password has been changed.')) text = text + ' ' + str(_('Your password has been changed.'))
if logentry.parsed_data.get('is_active') is True: if data.get('is_active') is True:
text = text + ' ' + str(_('Your account has been enabled.')) text = text + ' ' + str(_('Your account has been enabled.'))
elif logentry.parsed_data.get('is_active') is False: elif data.get('is_active') is False:
text = text + ' ' + str(_('Your account has been disabled.')) text = text + ' ' + str(_('Your account has been disabled.'))
return text return text
class UserImpersonatedLogEntryType(LogEntryType): class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry): def display(self, logentry, data):
return self.plain.format(logentry.parsed_data['other_email']) return self.plain.format(data['other_email'])
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
@@ -688,16 +726,13 @@ class CoreLogEntryType(LogEntryType):
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'), 'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'), 'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'), 'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'), 'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'), 'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.tickets.provider': _('The settings of a ticket output provider have been changed.'),
'pretix.event.payment.provider': _('The settings of a payment provider have been changed.'),
'pretix.event.live.activated': _('The shop has been taken live.'), 'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'), 'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'), 'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
@@ -717,6 +752,19 @@ class CoreEventLogEntryType(EventLogEntryType):
pass pass
@log_entry_types.new_from_dict({
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
})
class CheckinlistLogEntryType(EventLogEntryType):
object_link_wrapper = _('Check-in list {val}')
object_link_viewname = 'control:event.orders.checkinlists.edit'
object_link_argname = 'list'
content_type = CheckinList
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
'pretix.event.plugins.enabled': _('The plugin has been enabled.'), 'pretix.event.plugins.enabled': _('The plugin has been enabled.'),
'pretix.event.plugins.disabled': _('The plugin has been disabled.'), 'pretix.event.plugins.disabled': _('The plugin has been disabled.'),
@@ -724,7 +772,7 @@ class CoreEventLogEntryType(EventLogEntryType):
class EventPluginStateLogEntryType(EventLogEntryType): class EventPluginStateLogEntryType(EventLogEntryType):
object_link_wrapper = _('Plugin {val}') object_link_wrapper = _('Plugin {val}')
def get_object_link_info(self, logentry) -> dict: def get_object_link_info(self, logentry) -> Optional[dict]:
if 'plugin' in logentry.parsed_data: if 'plugin' in logentry.parsed_data:
app = app_cache.get(logentry.parsed_data['plugin']) app = app_cache.get(logentry.parsed_data['plugin'])
if app and hasattr(app, 'PretixPluginMeta'): if app and hasattr(app, 'PretixPluginMeta'):
@@ -759,17 +807,17 @@ class CoreItemLogEntryType(ItemLogEntryType):
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'), 'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
}) })
class VariationLogEntryType(ItemLogEntryType): class VariationLogEntryType(ItemLogEntryType):
def display(self, logentry): def display(self, logentry, data):
if 'value' not in logentry.parsed_data: if 'value' not in data:
# Backwards compatibility # Backwards compatibility
var = ItemVariation.objects.filter(id=logentry.parsed_data['id']).first() var = ItemVariation.objects.filter(id=data['id']).first()
if var: if var:
logentry.parsed_data['value'] = str(var.value) data['value'] = str(var.value)
else: else:
logentry.parsed_data['value'] = '?' data['value'] = '?'
else: else:
logentry.parsed_data['value'] = LazyI18nString(logentry.parsed_data['value']) data['value'] = LazyI18nString(data['value'])
return super().display(logentry) return super().display(logentry, data)
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
@@ -825,27 +873,27 @@ class CoreDiscountLogEntryType(DiscountLogEntryType):
class LegacyCheckinLogEntryType(OrderLogEntryType): class LegacyCheckinLogEntryType(OrderLogEntryType):
action_type = 'pretix.control.views.checkin' action_type = 'pretix.control.views.checkin'
def display(self, logentry): def display(self, logentry, data):
# deprecated # deprecated
dt = dateutil.parser.parse(logentry.parsed_data.get('datetime')) dt = dateutil.parser.parse(data.get('datetime'))
tz = logentry.event.timezone tz = logentry.event.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT") dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in logentry.parsed_data: if 'list' in data:
try: try:
checkin_list = logentry.event.checkin_lists.get(pk=logentry.parsed_data.get('list')).name checkin_list = logentry.event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist: except CheckinList.DoesNotExist:
checkin_list = _("(unknown)") checkin_list = _("(unknown)")
else: else:
checkin_list = _("(unknown)") checkin_list = _("(unknown)")
if logentry.parsed_data.get('first'): if data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format( return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'), posid=data.get('positionid'),
datetime=dt_formatted, datetime=dt_formatted,
list=checkin_list, list=checkin_list,
) )
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format( return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'), posid=data.get('positionid'),
datetime=dt_formatted, datetime=dt_formatted,
list=checkin_list list=checkin_list
) )

View File

@@ -172,7 +172,7 @@
{% if form.hidden_if_available %} {% if form.hidden_if_available %}
{% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %} {% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %}
{% endif %} {% endif %}
{% bootstrap_field form.hidden_if_item_available layout="control" horizontal_field_class="col-md-7" %} {% bootstrap_field form.hidden_if_item_available visibility_field=form.hidden_if_item_available_mode layout="control_with_visibility" %}
</fieldset> </fieldset>
{% for v in formsets.values %} {% for v in formsets.values %}
<fieldset> <fieldset>

View File

@@ -2,5 +2,5 @@
{% if mode == "hide" %} {% if mode == "hide" %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Hide product if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-eye-slash"></span></span> <span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Hide product if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-eye-slash"></span></span>
{% else %} {% else %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show info text if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span> <span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show product with info on why its unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span>
{% endif %} {% endif %}

View File

@@ -491,8 +491,11 @@ class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMix
if self.form.is_valid(): if self.form.is_valid():
if self.form.has_changed(): if self.form.has_changed():
self.request.event.log_action( self.request.event.log_action(
'pretix.event.payment.provider.' + self.provider.identifier, user=self.request.user, data={ 'pretix.event.payment.provider', user=self.request.user, data={
k: self.form.cleaned_data.get(k) for k in self.form.changed_data 'provider': self.provider.identifier,
'new_values': {
k: self.form.cleaned_data.get(k) for k in self.form.changed_data
}
} }
) )
self.form.save() self.form.save()
@@ -888,11 +891,14 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
provider.form.save() provider.form.save()
if provider.form.has_changed(): if provider.form.has_changed():
self.request.event.log_action( self.request.event.log_action(
'pretix.event.tickets.provider.' + provider.identifier, user=self.request.user, data={ 'pretix.event.tickets.provider', user=self.request.user, data={
k: (provider.form.cleaned_data.get(k).name 'provider': provider.identifier,
if isinstance(provider.form.cleaned_data.get(k), File) 'new_values': {
else provider.form.cleaned_data.get(k)) k: (provider.form.cleaned_data.get(k).name
for k in provider.form.changed_data if isinstance(provider.form.cleaned_data.get(k), File)
else provider.form.cleaned_data.get(k))
for k in provider.form.changed_data
}
} }
) )
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier}) tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier})

View File

@@ -313,7 +313,7 @@ class EventWizard(SafeSessionWizardView):
) )
event.set_defaults() event.set_defaults()
if basics_data['tax_rate']: if basics_data['tax_rate'] is not None:
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']: if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:
event.settings.tax_rate_default = event.tax_rules.create( event.settings.tax_rate_default = event.tax_rules.create(
name=LazyI18nString.from_gettext(gettext('VAT')), name=LazyI18nString.from_gettext(gettext('VAT')),

View File

@@ -62,7 +62,8 @@ class MetricsMiddleware(object):
t0 = time.perf_counter() t0 = time.perf_counter()
resp = self.get_response(request) resp = self.get_response(request)
tdiff = time.perf_counter() - t0 tdiff = time.perf_counter() - t0
pretix_view_duration_seconds.observe(tdiff, status_code=resp.status_code, method=request.method, if url.url_name:
url_name=url.namespace + ':' + url.url_name) pretix_view_duration_seconds.observe(tdiff, status_code=resp.status_code, method=request.method,
url_name=url.namespace + ':' + url.url_name)
return resp return resp

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-21 16:42+0000\n" "POT-Creation-Date: 2025-01-21 16:42+0000\n"
"PO-Revision-Date: 2024-11-24 01:00+0000\n" "PO-Revision-Date: 2025-01-25 22:00+0000\n"
"Last-Translator: gabriblas <github@unowen.simplelogin.com>\n" "Last-Translator: Rosariocastellana <rosariocastellana@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
"it/>\n" "it/>\n"
"Language: it\n" "Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.3\n" "X-Generator: Weblate 5.9.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -93,7 +93,7 @@ msgstr "Italiano"
#: pretix/_base_settings.py:105 #: pretix/_base_settings.py:105
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr "Giapponese"
#: pretix/_base_settings.py:106 #: pretix/_base_settings.py:106
msgid "Latvian" msgid "Latvian"
@@ -3415,6 +3415,8 @@ msgid ""
"The relevant plugin is currently not active. To activate it, click here to " "The relevant plugin is currently not active. To activate it, click here to "
"go to the plugin settings." "go to the plugin settings."
msgstr "" msgstr ""
"Il plugin in questione non è al momento attivo. Per attivarlo, clicca qui "
"per andare alle impostazioni del plugin."
#: pretix/base/logentrytypes.py:50 #: pretix/base/logentrytypes.py:50
#, fuzzy #, fuzzy
@@ -3425,48 +3427,48 @@ msgstr "Questo prodotto non è al momento disponibile."
#: pretix/base/logentrytypes.py:183 #: pretix/base/logentrytypes.py:183
#, python-brace-format #, python-brace-format
msgid "Order {val}" msgid "Order {val}"
msgstr "" msgstr "Ordine {val}"
#: pretix/base/logentrytypes.py:194 #: pretix/base/logentrytypes.py:194
#, python-brace-format #, python-brace-format
msgid "Voucher {val}…" msgid "Voucher {val}…"
msgstr "" msgstr "Voucher {val}…"
#: pretix/base/logentrytypes.py:205 #: pretix/base/logentrytypes.py:205
#, python-brace-format #, python-brace-format
msgid "Product {val}" msgid "Product {val}"
msgstr "" msgstr "Prodotto {val}"
#: pretix/base/logentrytypes.py:211 #: pretix/base/logentrytypes.py:211
#, python-brace-format #, python-brace-format
msgctxt "subevent" msgctxt "subevent"
msgid "Date {val}" msgid "Date {val}"
msgstr "" msgstr "Data {val}"
#: pretix/base/logentrytypes.py:217 #: pretix/base/logentrytypes.py:217
#, python-brace-format #, python-brace-format
msgid "Quota {val}" msgid "Quota {val}"
msgstr "" msgstr "Quota {val}"
#: pretix/base/logentrytypes.py:223 #: pretix/base/logentrytypes.py:223
#, python-brace-format #, python-brace-format
msgid "Discount {val}" msgid "Discount {val}"
msgstr "" msgstr "Sconto {val}"
#: pretix/base/logentrytypes.py:229 #: pretix/base/logentrytypes.py:229
#, python-brace-format #, python-brace-format
msgid "Category {val}" msgid "Category {val}"
msgstr "" msgstr "Categoria {val}"
#: pretix/base/logentrytypes.py:235 #: pretix/base/logentrytypes.py:235
#, python-brace-format #, python-brace-format
msgid "Question {val}" msgid "Question {val}"
msgstr "" msgstr "Domanda {val}"
#: pretix/base/logentrytypes.py:241 #: pretix/base/logentrytypes.py:241
#, python-brace-format #, python-brace-format
msgid "Tax rule {val}" msgid "Tax rule {val}"
msgstr "" msgstr "Regola fiscale {val}"
#: pretix/base/media.py:71 #: pretix/base/media.py:71
msgid "Barcode / QR-Code" msgid "Barcode / QR-Code"
@@ -4771,7 +4773,7 @@ msgstr ""
#: pretix/base/models/items.py:455 #: pretix/base/models/items.py:455
msgid "Require either an existing or a new medium to be used" msgid "Require either an existing or a new medium to be used"
msgstr "" msgstr "Richiede l'utilizzo di un mezzo esistente o di uno nuovo"
#: pretix/base/models/items.py:471 pretix/base/models/items.py:1446 #: pretix/base/models/items.py:471 pretix/base/models/items.py:1446
msgid "Category" msgid "Category"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-21 16:42+0000\n" "POT-Creation-Date: 2025-01-21 16:42+0000\n"
"PO-Revision-Date: 2025-01-22 16:00+0000\n" "PO-Revision-Date: 2025-01-27 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n" "ja/>\n"
@@ -2956,7 +2956,7 @@ msgstr ""
#: pretix/base/forms/user.py:56 pretix/control/forms/users.py:45 #: pretix/base/forms/user.py:56 pretix/control/forms/users.py:45
#: pretix/presale/forms/customer.py:283 pretix/presale/forms/customer.py:371 #: pretix/presale/forms/customer.py:283 pretix/presale/forms/customer.py:371
msgid "Please enter the same password twice" msgid "Please enter the same password twice"
msgstr "同じパスワードを2回入力してください" msgstr "同じパスワードを回入力してください"
#: pretix/base/forms/auth.py:172 pretix/base/forms/auth.py:224 #: pretix/base/forms/auth.py:172 pretix/base/forms/auth.py:224
#: pretix/presale/forms/customer.py:296 pretix/presale/forms/customer.py:390 #: pretix/presale/forms/customer.py:296 pretix/presale/forms/customer.py:390
@@ -3401,13 +3401,12 @@ msgstr "イベントの日程: {date_range}"
msgid "" msgid ""
"The relevant plugin is currently not active. To activate it, click here to " "The relevant plugin is currently not active. To activate it, click here to "
"go to the plugin settings." "go to the plugin settings."
msgstr "" msgstr "関連するプラグインは、現在アクティブではありません。アクティブにするには、こ"
"こをクリックしてプラグイン設定に進んでください。"
#: pretix/base/logentrytypes.py:50 #: pretix/base/logentrytypes.py:50
#, fuzzy
#| msgid "This product is currently not available."
msgid "The relevant plugin is currently not active." msgid "The relevant plugin is currently not active."
msgstr "この製品は現在利用できません。" msgstr "関連するプラグインは、現在無効です。"
#: pretix/base/logentrytypes.py:183 #: pretix/base/logentrytypes.py:183
#, python-brace-format #, python-brace-format
@@ -16353,22 +16352,17 @@ msgid "A user has been removed from the event team."
msgstr "ユーザーがイベントチームから削除されました。" msgstr "ユーザーがイベントチームから削除されました。"
#: pretix/control/logdisplay.py:721 #: pretix/control/logdisplay.py:721
#, fuzzy
#| msgid "A plugin has been enabled."
msgid "The plugin has been enabled." msgid "The plugin has been enabled."
msgstr "プラグインが有効になりました。" msgstr "プラグインが有効になりました。"
#: pretix/control/logdisplay.py:722 #: pretix/control/logdisplay.py:722
#, fuzzy
#| msgid "A plugin has been disabled."
msgid "The plugin has been disabled." msgid "The plugin has been disabled."
msgstr "プラグインが無効になっています。" msgstr "プラグインが無効になっています。"
#: pretix/control/logdisplay.py:725 #: pretix/control/logdisplay.py:725
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "Question {val}"
msgid "Plugin {val}" msgid "Plugin {val}"
msgstr "質問{val}" msgstr "プラグイン{val}"
#: pretix/control/logdisplay.py:741 #: pretix/control/logdisplay.py:741
msgid "The product has been created." msgid "The product has been created."
@@ -29723,10 +29717,9 @@ msgid "An email rule was deleted"
msgstr "電子メールのルールが削除されました" msgstr "電子メールのルールが削除されました"
#: pretix/plugins/sendmail/signals.py:140 #: pretix/plugins/sendmail/signals.py:140
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "Tax rule {val}"
msgid "Mail rule {val}" msgid "Mail rule {val}"
msgstr "税金のルール {val}" msgstr "メールのルール {val}"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html:8 #: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html:8
msgid "" msgid ""
@@ -32173,7 +32166,7 @@ msgstr "この製品の価格が自動割引によって削減されました。
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:346 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:346
#, python-format #, python-format
msgid "%(percent)s %% Discount" msgid "%(percent)s %% Discount"
msgstr "" msgstr "%(percent)s %% 割引"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:277 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:277
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:350 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:350

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-21 16:42+0000\n" "POT-Creation-Date: 2025-01-21 16:42+0000\n"
"PO-Revision-Date: 2024-11-14 20:00+0000\n" "PO-Revision-Date: 2025-01-28 01:00+0000\n"
"Last-Translator: Gravity Fox <gravityraposa@gmail.com>\n" "Last-Translator: Cornelius Kibelka <ckibelka-ctr@wikimedia.org>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n" "pretix/pretix/pt_BR/>\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.8.3\n" "X-Generator: Weblate 5.9.2\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -93,7 +93,7 @@ msgstr "Italiano"
#: pretix/_base_settings.py:105 #: pretix/_base_settings.py:105
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr "Japonês"
#: pretix/_base_settings.py:106 #: pretix/_base_settings.py:106
msgid "Latvian" msgid "Latvian"
@@ -2436,10 +2436,8 @@ msgid "Download a spreadsheet of all payments or refunds of every order."
msgstr "Baixe uma planilha de todos os pagamentos e reembolsos de cada pedido." msgstr "Baixe uma planilha de todos os pagamentos e reembolsos de cada pedido."
#: pretix/base/exporters/orderlist.py:1012 #: pretix/base/exporters/orderlist.py:1012
#, fuzzy
#| msgid "Date of last payment"
msgid "Date range (payment date)" msgid "Date range (payment date)"
msgstr "Data do último pagamento" msgstr "Intervalo de datas (data de pagamento)"
#: pretix/base/exporters/orderlist.py:1015 #: pretix/base/exporters/orderlist.py:1015
msgid "" msgid ""
@@ -2450,7 +2448,7 @@ msgstr ""
#: pretix/base/exporters/orderlist.py:1019 #: pretix/base/exporters/orderlist.py:1019
msgid "Date range (start of transaction)" msgid "Date range (start of transaction)"
msgstr "" msgstr "Intervalo de datas (início da transação)"
#: pretix/base/exporters/orderlist.py:1025 #: pretix/base/exporters/orderlist.py:1025
msgid "Payment states" msgid "Payment states"
@@ -2576,10 +2574,8 @@ msgid "Current user's carts"
msgstr "Carrinhos de usuários atuais" msgstr "Carrinhos de usuários atuais"
#: pretix/base/exporters/orderlist.py:1135 #: pretix/base/exporters/orderlist.py:1135
#, fuzzy
#| msgid "Paid orders"
msgid "Exited orders" msgid "Exited orders"
msgstr "Ordens pagas" msgstr "Pedidos encerrados"
#: pretix/base/exporters/orderlist.py:1135 #: pretix/base/exporters/orderlist.py:1135
msgid "Current availability" msgid "Current availability"
@@ -2593,22 +2589,19 @@ msgid "Infinite"
msgstr "Infinito" msgstr "Infinito"
#: pretix/base/exporters/orderlist.py:1181 #: pretix/base/exporters/orderlist.py:1181
#, fuzzy
#| msgid "Gift card"
msgid "Gift card transactions" msgid "Gift card transactions"
msgstr "Cartão Presente" msgstr "Transações com cartões-presente"
#: pretix/base/exporters/orderlist.py:1183 #: pretix/base/exporters/orderlist.py:1183
#: pretix/base/exporters/orderlist.py:1288 #: pretix/base/exporters/orderlist.py:1288
#, fuzzy
#| msgid "Gift card"
msgctxt "export_category" msgctxt "export_category"
msgid "Gift cards" msgid "Gift cards"
msgstr "Cartão Presente" msgstr "Cartões-presente"
#: pretix/base/exporters/orderlist.py:1184 #: pretix/base/exporters/orderlist.py:1184
msgid "Download a spreadsheet of all gift card transactions." msgid "Download a spreadsheet of all gift card transactions."
msgstr "" msgstr ""
"Faça o download de uma planilha de todas as transações com cartões-presente."
#: pretix/base/exporters/orderlist.py:1212 #: pretix/base/exporters/orderlist.py:1212
#: pretix/base/exporters/orderlist.py:1259 #: pretix/base/exporters/orderlist.py:1259
@@ -2619,10 +2612,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/giftcard.html:28 #: pretix/control/templates/pretixcontrol/organizers/giftcard.html:28
#: pretix/control/templates/pretixcontrol/organizers/giftcards.html:56 #: pretix/control/templates/pretixcontrol/organizers/giftcards.html:56
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:156 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:156
#, fuzzy
#| msgid "Gift card"
msgid "Gift card code" msgid "Gift card code"
msgstr "Cartão Presente" msgstr "Código do cartão-presente"
#: pretix/base/exporters/orderlist.py:1213 #: pretix/base/exporters/orderlist.py:1213
#: pretix/base/exporters/orderlist.py:1302 #: pretix/base/exporters/orderlist.py:1302
@@ -2631,7 +2622,7 @@ msgstr "Cartão Presente"
#: pretix/control/forms/filter.py:1413 pretix/control/forms/filter.py:1416 #: pretix/control/forms/filter.py:1413 pretix/control/forms/filter.py:1416
#: pretix/control/templates/pretixcontrol/event/live.html:75 #: pretix/control/templates/pretixcontrol/event/live.html:75
msgid "Test mode" msgid "Test mode"
msgstr "" msgstr "Modo de teste"
#: pretix/base/exporters/orderlist.py:1218 pretix/base/models/organizer.py:96 #: pretix/base/exporters/orderlist.py:1218 pretix/base/models/organizer.py:96
#: pretix/control/forms/event.py:110 pretix/control/forms/event.py:116 #: pretix/control/forms/event.py:110 pretix/control/forms/event.py:116
@@ -2678,41 +2669,41 @@ msgid "TEST MODE"
msgstr "MODO DE TESTE" msgstr "MODO DE TESTE"
#: pretix/base/exporters/orderlist.py:1240 #: pretix/base/exporters/orderlist.py:1240
#, fuzzy
#| msgid "Gift card"
msgid "Gift card redemptions" msgid "Gift card redemptions"
msgstr "Cartão Presente" msgstr "Resgates de cartões-presente"
#: pretix/base/exporters/orderlist.py:1242 #: pretix/base/exporters/orderlist.py:1242
msgid "" msgid ""
"Download a spreadsheet of all payments or refunds that involve gift cards." "Download a spreadsheet of all payments or refunds that involve gift cards."
msgstr "" msgstr ""
"Faça o download de uma planilha de todos os pagamentos ou reembolsos que "
"envolvam cartões-presente."
#: pretix/base/exporters/orderlist.py:1259 #: pretix/base/exporters/orderlist.py:1259
#: pretix/control/templates/pretixcontrol/giftcards/payment.html:16 #: pretix/control/templates/pretixcontrol/giftcards/payment.html:16
msgid "Issuer" msgid "Issuer"
msgstr "" msgstr "Emissor"
#: pretix/base/exporters/orderlist.py:1286 pretix/control/navigation.py:538 #: pretix/base/exporters/orderlist.py:1286 pretix/control/navigation.py:538
#: pretix/control/navigation.py:556 #: pretix/control/navigation.py:556
#: pretix/control/templates/pretixcontrol/organizers/edit.html:156 #: pretix/control/templates/pretixcontrol/organizers/edit.html:156
#: pretix/plugins/reports/accountingreport.py:898 #: pretix/plugins/reports/accountingreport.py:898
#, fuzzy
#| msgid "Gift card"
msgid "Gift cards" msgid "Gift cards"
msgstr "Cartão Presente" msgstr "Cartões-presente"
#: pretix/base/exporters/orderlist.py:1289 #: pretix/base/exporters/orderlist.py:1289
msgid "Download a spreadsheet of all gift cards including their current value." msgid "Download a spreadsheet of all gift cards including their current value."
msgstr "" msgstr ""
"Faça o download de uma planilha de todos os cartões-presente, incluindo seu "
"valor atual."
#: pretix/base/exporters/orderlist.py:1296 #: pretix/base/exporters/orderlist.py:1296
msgid "Show value at" msgid "Show value at"
msgstr "" msgstr "Mostrar o valor em"
#: pretix/base/exporters/orderlist.py:1299 #: pretix/base/exporters/orderlist.py:1299
msgid "Defaults to the time of report." msgid "Defaults to the time of report."
msgstr "" msgstr "Por padrão, a hora do relatório."
#: pretix/base/exporters/orderlist.py:1304 #: pretix/base/exporters/orderlist.py:1304
#: pretix/base/exporters/orderlist.py:1314 pretix/control/forms/filter.py:518 #: pretix/base/exporters/orderlist.py:1314 pretix/control/forms/filter.py:518
@@ -2738,20 +2729,20 @@ msgstr "Todos"
#: pretix/base/exporters/orderlist.py:1306 pretix/control/forms/filter.py:1417 #: pretix/base/exporters/orderlist.py:1306 pretix/control/forms/filter.py:1417
msgid "Live" msgid "Live"
msgstr "" msgstr "Em tempo real"
#: pretix/base/exporters/orderlist.py:1315 pretix/control/forms/filter.py:1425 #: pretix/base/exporters/orderlist.py:1315 pretix/control/forms/filter.py:1425
#: pretix/control/templates/pretixcontrol/pdf/index.html:252 #: pretix/control/templates/pretixcontrol/pdf/index.html:252
msgid "Empty" msgid "Empty"
msgstr "" msgstr "Vazio"
#: pretix/base/exporters/orderlist.py:1316 pretix/control/forms/filter.py:1426 #: pretix/base/exporters/orderlist.py:1316 pretix/control/forms/filter.py:1426
msgid "Valid and with value" msgid "Valid and with value"
msgstr "" msgstr "Válido e com valor"
#: pretix/base/exporters/orderlist.py:1317 pretix/control/forms/filter.py:1427 #: pretix/base/exporters/orderlist.py:1317 pretix/control/forms/filter.py:1427
msgid "Expired and with value" msgid "Expired and with value"
msgstr "" msgstr "Expirado e com valor"
#: pretix/base/exporters/orderlist.py:1318 pretix/control/forms/filter.py:227 #: pretix/base/exporters/orderlist.py:1318 pretix/control/forms/filter.py:227
#: pretix/control/forms/filter.py:1428 pretix/control/forms/filter.py:2097 #: pretix/control/forms/filter.py:1428 pretix/control/forms/filter.py:2097
@@ -2766,14 +2757,14 @@ msgstr "Expirado"
#: pretix/base/exporters/orderlist.py:1356 pretix/base/models/giftcards.py:98 #: pretix/base/exporters/orderlist.py:1356 pretix/base/models/giftcards.py:98
msgid "Test mode card" msgid "Test mode card"
msgstr "" msgstr "Cartão de modo de teste"
#: pretix/base/exporters/orderlist.py:1358 #: pretix/base/exporters/orderlist.py:1358
#: pretix/base/modelimport_orders.py:516 pretix/base/models/giftcards.py:102 #: pretix/base/modelimport_orders.py:516 pretix/base/models/giftcards.py:102
#: pretix/control/templates/pretixcontrol/order/index.html:202 #: pretix/control/templates/pretixcontrol/order/index.html:202
#: pretix/control/templates/pretixcontrol/organizers/giftcards.html:62 #: pretix/control/templates/pretixcontrol/organizers/giftcards.html:62
msgid "Expiry date" msgid "Expiry date"
msgstr "" msgstr "Data de expiração"
#: pretix/base/exporters/orderlist.py:1359 pretix/control/forms/orders.py:875 #: pretix/base/exporters/orderlist.py:1359 pretix/control/forms/orders.py:875
msgid "Special terms and conditions" msgid "Special terms and conditions"
@@ -2792,17 +2783,12 @@ msgid "Created in order"
msgstr "Criado em ordem" msgstr "Criado em ordem"
#: pretix/base/exporters/orderlist.py:1363 #: pretix/base/exporters/orderlist.py:1363
#, fuzzy
#| msgctxt "invoice"
#| msgid "Invoice number"
msgid "Last invoice number of order" msgid "Last invoice number of order"
msgstr "Número da fatura" msgstr "Último número de fatura do pedido"
#: pretix/base/exporters/orderlist.py:1364 #: pretix/base/exporters/orderlist.py:1364
#, fuzzy
#| msgid "Only paid orders"
msgid "Last invoice date of order" msgid "Last invoice date of order"
msgstr "Apenas ordens pagas" msgstr "Data da última fatura do pedido"
#: pretix/base/exporters/reusablemedia.py:34 pretix/control/navigation.py:616 #: pretix/base/exporters/reusablemedia.py:34 pretix/control/navigation.py:616
#: pretix/control/templates/pretixcontrol/organizers/edit.html:204 #: pretix/control/templates/pretixcontrol/organizers/edit.html:204
@@ -2830,11 +2816,9 @@ msgid "Media type"
msgstr "Tipo de mídia" msgstr "Tipo de mídia"
#: pretix/base/exporters/reusablemedia.py:47 pretix/base/models/media.py:73 #: pretix/base/exporters/reusablemedia.py:47 pretix/base/models/media.py:73
#, fuzzy
#| msgid "Internal identifier"
msgctxt "reusable_medium" msgctxt "reusable_medium"
msgid "Identifier" msgid "Identifier"
msgstr "Identificador interno" msgstr "Identificador"
#: pretix/base/exporters/reusablemedia.py:49 pretix/base/models/media.py:81 #: pretix/base/exporters/reusablemedia.py:49 pretix/base/models/media.py:81
#: pretix/base/models/orders.py:265 pretix/base/models/orders.py:3094 #: pretix/base/models/orders.py:265 pretix/base/models/orders.py:3094
@@ -2846,27 +2830,18 @@ msgstr "Data de validade"
#: pretix/base/exporters/reusablemedia.py:50 pretix/base/models/media.py:90 #: pretix/base/exporters/reusablemedia.py:50 pretix/base/models/media.py:90
#: pretix/control/templates/pretixcontrol/order/index.html:215 #: pretix/control/templates/pretixcontrol/order/index.html:215
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:132 #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:132
#, fuzzy
#| msgctxt "refund_source"
#| msgid "Customer"
msgid "Customer account" msgid "Customer account"
msgstr "Cliente" msgstr "Conta do cliente"
#: pretix/base/exporters/reusablemedia.py:51 pretix/base/models/media.py:97 #: pretix/base/exporters/reusablemedia.py:51 pretix/base/models/media.py:97
#, fuzzy
#| msgid "Multiline text"
msgid "Linked ticket" msgid "Linked ticket"
msgstr "Texto multilinha" msgstr "Bilhete vinculado"
#: pretix/base/exporters/reusablemedia.py:52 pretix/base/models/media.py:104 #: pretix/base/exporters/reusablemedia.py:52 pretix/base/models/media.py:104
#, fuzzy
#| msgid "Gift card"
msgid "Linked gift card" msgid "Linked gift card"
msgstr "Cartão Presente" msgstr "Cartão-presente vinculado"
#: pretix/base/exporters/waitinglist.py:42 #: pretix/base/exporters/waitinglist.py:42
#, fuzzy
#| msgid "Waiting list"
msgctxt "export_category" msgctxt "export_category"
msgid "Waiting list" msgid "Waiting list"
msgstr "Lista de espera" msgstr "Lista de espera"
@@ -2874,23 +2849,24 @@ msgstr "Lista de espera"
#: pretix/base/exporters/waitinglist.py:43 #: pretix/base/exporters/waitinglist.py:43
msgid "Download a spread sheet with all your waiting list data." msgid "Download a spread sheet with all your waiting list data."
msgstr "" msgstr ""
"Faça o download de uma planilha com todos os dados de sua lista de espera."
#: pretix/base/exporters/waitinglist.py:49 #: pretix/base/exporters/waitinglist.py:49
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:102 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:102
msgid "All entries" msgid "All entries"
msgstr "" msgstr "Todos os registros"
#: pretix/base/exporters/waitinglist.py:54 #: pretix/base/exporters/waitinglist.py:54
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:105 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:105
msgid "Waiting for a voucher" msgid "Waiting for a voucher"
msgstr "" msgstr "Aguardando um voucher"
#: pretix/base/exporters/waitinglist.py:59 #: pretix/base/exporters/waitinglist.py:59
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:107 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:107
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:227 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:227
#: pretix/control/views/waitinglist.py:326 #: pretix/control/views/waitinglist.py:326
msgid "Voucher assigned" msgid "Voucher assigned"
msgstr "" msgstr "Voucher atribuído"
#: pretix/base/exporters/waitinglist.py:64 #: pretix/base/exporters/waitinglist.py:64
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:110 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:110
@@ -2903,7 +2879,7 @@ msgstr "Descrição da categoria"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:223 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:223
#: pretix/control/views/waitinglist.py:322 #: pretix/control/views/waitinglist.py:322
msgid "Voucher redeemed" msgid "Voucher redeemed"
msgstr "" msgstr "Voucher resgatado"
#: pretix/base/exporters/waitinglist.py:80 #: pretix/base/exporters/waitinglist.py:80
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:116 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:116
@@ -2918,7 +2894,7 @@ msgstr "O carrinho expirou"
#: pretix/control/forms/event.py:1764 #: pretix/control/forms/event.py:1764
#: pretix/control/templates/pretixcontrol/items/index.html:38 #: pretix/control/templates/pretixcontrol/items/index.html:38
msgid "Product name" msgid "Product name"
msgstr "" msgstr "Nome do produto"
#: pretix/base/exporters/waitinglist.py:115 #: pretix/base/exporters/waitinglist.py:115
#: pretix/base/modelimport_orders.py:95 pretix/base/modelimport_vouchers.py:60 #: pretix/base/modelimport_orders.py:95 pretix/base/modelimport_vouchers.py:60
@@ -2932,7 +2908,7 @@ msgstr "Data"
#: pretix/base/exporters/waitinglist.py:119 #: pretix/base/exporters/waitinglist.py:119
#: pretix/control/views/waitinglist.py:308 #: pretix/control/views/waitinglist.py:308
msgid "Priority" msgid "Priority"
msgstr "" msgstr "Prioridade"
#: pretix/base/exporters/waitinglist.py:121 #: pretix/base/exporters/waitinglist.py:121
#: pretix/base/modelimport_vouchers.py:39 pretix/base/models/vouchers.py:190 #: pretix/base/modelimport_vouchers.py:39 pretix/base/models/vouchers.py:190
@@ -2951,7 +2927,7 @@ msgstr "Código do voucher"
#: pretix/base/forms/__init__.py:118 #: pretix/base/forms/__init__.py:118
#, python-brace-format #, python-brace-format
msgid "You can use {markup_name} in this field." msgid "You can use {markup_name} in this field."
msgstr "" msgstr "Você pode usar {markup_name} nesse campo."
#: pretix/base/forms/__init__.py:178 #: pretix/base/forms/__init__.py:178
#, python-format #, python-format
@@ -2959,6 +2935,8 @@ msgid ""
"Due to technical reasons you cannot set inputs, that need to be masked (e.g. " "Due to technical reasons you cannot set inputs, that need to be masked (e.g. "
"passwords), to %(value)s." "passwords), to %(value)s."
msgstr "" msgstr ""
"Por motivos técnicos, não é possível definir entradas que precisam ser "
"mascaradas (por exemplo, senhas), como %(value)s."
#: pretix/base/forms/auth.py:61 pretix/base/forms/auth.py:179 #: pretix/base/forms/auth.py:61 pretix/base/forms/auth.py:179
msgid "Keep me logged in" msgid "Keep me logged in"
@@ -2966,12 +2944,12 @@ msgstr "Mantenha-me conectado"
#: pretix/base/forms/auth.py:65 pretix/base/forms/auth.py:272 #: pretix/base/forms/auth.py:65 pretix/base/forms/auth.py:272
msgid "This combination of credentials is not known to our system." msgid "This combination of credentials is not known to our system."
msgstr "" msgstr "Essa combinação de credenciais não é conhecida pelo nosso sistema."
#: pretix/base/forms/auth.py:66 pretix/base/forms/user.py:57 #: pretix/base/forms/auth.py:66 pretix/base/forms/user.py:57
#: pretix/presale/forms/customer.py:372 pretix/presale/forms/customer.py:444 #: pretix/presale/forms/customer.py:372 pretix/presale/forms/customer.py:444
msgid "For security reasons, please wait 5 minutes before you try again." msgid "For security reasons, please wait 5 minutes before you try again."
msgstr "" msgstr "Por motivos de segurança, aguarde 5 minutos antes de tentar novamente."
#: pretix/base/forms/auth.py:67 pretix/base/forms/auth.py:273 #: pretix/base/forms/auth.py:67 pretix/base/forms/auth.py:273
msgid "This account is inactive." msgid "This account is inactive."
@@ -3018,35 +2996,43 @@ msgstr "Referência interna"
#: pretix/base/forms/questions.py:320 #: pretix/base/forms/questions.py:320
msgctxt "phonenumber" msgctxt "phonenumber"
msgid "Phone number (without international area code)" msgid "Phone number (without international area code)"
msgstr "" msgstr "Número de telefone (sem código de área internacional)"
#: pretix/base/forms/questions.py:481 #: pretix/base/forms/questions.py:481
msgid "" msgid ""
"You uploaded an image in landscape orientation. Please upload an image in " "You uploaded an image in landscape orientation. Please upload an image in "
"portrait orientation." "portrait orientation."
msgstr "" msgstr ""
"Você fez o upload de uma imagem na orientação paisagem. Carregue uma imagem "
"na orientação retrato."
#: pretix/base/forms/questions.py:484 #: pretix/base/forms/questions.py:484
msgid "Please upload an image where the width is 3/4 of the height." msgid "Please upload an image where the width is 3/4 of the height."
msgstr "" msgstr "Carregue uma imagem em que a largura seja 3/4 da altura."
#: pretix/base/forms/questions.py:487 #: pretix/base/forms/questions.py:487
msgid "" msgid ""
"The file you uploaded has a very large number of pixels, please upload an " "The file you uploaded has a very large number of pixels, please upload an "
"image no larger than 10000 x 10000 pixels." "image no larger than 10000 x 10000 pixels."
msgstr "" msgstr ""
"O arquivo que você carregou tem um número muito grande de pixels. Carregue "
"uma imagem que não seja maior que 10.000 x 10.000 pixels."
#: pretix/base/forms/questions.py:490 pretix/helpers/images.py:75 #: pretix/base/forms/questions.py:490 pretix/helpers/images.py:75
msgid "" msgid ""
"Upload a valid image. The file you uploaded was either not an image or a " "Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image." "corrupted image."
msgstr "" msgstr ""
"Faça upload de uma imagem válida. O arquivo que você carregou não era uma "
"imagem ou era uma imagem corrompida."
#: pretix/base/forms/questions.py:633 pretix/base/forms/questions.py:642 #: pretix/base/forms/questions.py:633 pretix/base/forms/questions.py:642
msgid "" msgid ""
"If you keep this empty, the ticket will be valid starting at the time of " "If you keep this empty, the ticket will be valid starting at the time of "
"purchase." "purchase."
msgstr "" msgstr ""
"Se você mantiver esse campo vazio, o bilhete será válido a partir do momento "
"da compra."
#: pretix/base/forms/questions.py:689 pretix/base/forms/questions.py:1014 #: pretix/base/forms/questions.py:689 pretix/base/forms/questions.py:1014
msgid "Street and Number" msgid "Street and Number"
@@ -3064,16 +3050,20 @@ msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
"you additional taxes if you do not enter it." "you additional taxes if you do not enter it."
msgstr "" msgstr ""
"Opcional, mas, dependendo do país em que você reside, talvez seja necessário "
"cobrar impostos adicionais se você não os inserir."
#: pretix/base/forms/questions.py:1071 pretix/base/forms/questions.py:1077 #: pretix/base/forms/questions.py:1071 pretix/base/forms/questions.py:1077
msgid "If you are registered in Switzerland, you can enter your UID instead." msgid "If you are registered in Switzerland, you can enter your UID instead."
msgstr "" msgstr "Se você estiver registrado na Suíça, poderá inserir seu UID."
#: pretix/base/forms/questions.py:1075 #: pretix/base/forms/questions.py:1075
msgid "" msgid ""
"Optional, but it might be required for you to claim tax benefits on your " "Optional, but it might be required for you to claim tax benefits on your "
"invoice depending on your and the sellers country of residence." "invoice depending on your and the sellers country of residence."
msgstr "" msgstr ""
"Opcional, mas pode ser necessário para que você solicite benefícios fiscais "
"em sua fatura, dependendo do seu país de residência e do país do vendedor."
#: pretix/base/forms/questions.py:1174 #: pretix/base/forms/questions.py:1174
msgid "You need to provide a company name." msgid "You need to provide a company name."
@@ -3109,7 +3099,7 @@ msgstr "Senha incorreta."
#: pretix/base/forms/user.py:58 #: pretix/base/forms/user.py:58
msgid "Please choose a password different to your current one." msgid "Please choose a password different to your current one."
msgstr "" msgstr "Escolha uma senha diferente da atual."
#: pretix/base/forms/user.py:63 pretix/presale/forms/customer.py:379 #: pretix/base/forms/user.py:63 pretix/presale/forms/customer.py:379
#: pretix/presale/forms/customer.py:448 #: pretix/presale/forms/customer.py:448
@@ -3164,6 +3154,10 @@ msgid ""
"up. Please note: to use literal \"{\" or \"}\", you need to double them as " "up. Please note: to use literal \"{\" or \"}\", you need to double them as "
"\"{{\" and \"}}\"." "\"{{\" and \"}}\"."
msgstr "" msgstr ""
"Há um erro na sintaxe de seu placeholder. Verifique se os colchetes “{” de "
"abertura e “}” de fechamento em seus placeholders estão corretos. Observação:"
" para usar literalmente “{” ou “}”, você precisa duplicá-los como “{{” e "
"“}}”."
#: pretix/base/forms/validators.py:72 pretix/control/views/event.py:758 #: pretix/base/forms/validators.py:72 pretix/control/views/event.py:758
#, fuzzy, python-format #, fuzzy, python-format
@@ -3348,7 +3342,7 @@ msgstr "Montante"
#, python-brace-format #, python-brace-format
msgctxt "invoice" msgctxt "invoice"
msgid "Single price: {net_price} net / {gross_price} gross" msgid "Single price: {net_price} net / {gross_price} gross"
msgstr "" msgstr "Preço único: {net_price} líquido / {gross_price} bruto"
#: pretix/base/invoice.py:668 #: pretix/base/invoice.py:668
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -3372,7 +3366,7 @@ msgstr "Ordens pagas"
#: pretix/base/invoice.py:707 #: pretix/base/invoice.py:707
msgctxt "invoice" msgctxt "invoice"
msgid "Outstanding payments" msgid "Outstanding payments"
msgstr "" msgstr "Pagamentos pendentes"
#: pretix/base/invoice.py:724 #: pretix/base/invoice.py:724
#, fuzzy #, fuzzy
@@ -3438,7 +3432,7 @@ msgstr ""
#: pretix/base/invoice.py:867 #: pretix/base/invoice.py:867
msgid "Default invoice renderer (European-style letter)" msgid "Default invoice renderer (European-style letter)"
msgstr "" msgstr "Renderizador de faturas padrão (letra no estilo europeu)"
#: pretix/base/invoice.py:956 #: pretix/base/invoice.py:956
#, fuzzy #, fuzzy
@@ -3450,7 +3444,7 @@ msgstr "Você precisa selecionar uma data."
#: pretix/base/invoice.py:1003 #: pretix/base/invoice.py:1003
msgid "Simplified invoice renderer" msgid "Simplified invoice renderer"
msgstr "" msgstr "Renderizador de faturas simplificado"
#: pretix/base/invoice.py:1022 #: pretix/base/invoice.py:1022
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -3465,6 +3459,8 @@ msgid ""
"The relevant plugin is currently not active. To activate it, click here to " "The relevant plugin is currently not active. To activate it, click here to "
"go to the plugin settings." "go to the plugin settings."
msgstr "" msgstr ""
"O plug-in relevante não está ativo no momento. Para ativá-lo, clique aqui "
"para acessar as configurações do plug-in."
#: pretix/base/logentrytypes.py:50 #: pretix/base/logentrytypes.py:50
#, fuzzy #, fuzzy
@@ -3496,7 +3492,7 @@ msgstr "Data {val}"
#: pretix/base/logentrytypes.py:217 #: pretix/base/logentrytypes.py:217
#, python-brace-format #, python-brace-format
msgid "Quota {val}" msgid "Quota {val}"
msgstr "" msgstr "Contingente {val}"
#: pretix/base/logentrytypes.py:223 #: pretix/base/logentrytypes.py:223
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -3521,12 +3517,12 @@ msgstr "Norma fiscal {val}"
#: pretix/base/media.py:71 #: pretix/base/media.py:71
msgid "Barcode / QR-Code" msgid "Barcode / QR-Code"
msgstr "" msgstr "Código de barras / código QR"
#: pretix/base/media.py:88 #: pretix/base/media.py:88
#: pretix/control/templates/pretixcontrol/organizers/edit.html:237 #: pretix/control/templates/pretixcontrol/organizers/edit.html:237
msgid "NFC UID-based" msgid "NFC UID-based"
msgstr "" msgstr "Baseado em NFC UID"
#: pretix/base/migrations/0077_auto_20171124_1629.py:33 #: pretix/base/migrations/0077_auto_20171124_1629.py:33
#: pretix/base/migrations/0077_auto_20171124_1629_squashed_0088_auto_20180328_1217.py:35 #: pretix/base/migrations/0077_auto_20171124_1629_squashed_0088_auto_20180328_1217.py:35
@@ -3535,28 +3531,28 @@ msgstr "Lista padrão"
#: pretix/base/modelimport.py:112 #: pretix/base/modelimport.py:112
msgid "Keep empty" msgid "Keep empty"
msgstr "" msgstr "Manter vazio"
#: pretix/base/modelimport.py:139 #: pretix/base/modelimport.py:139
#, python-brace-format #, python-brace-format
msgid "Invalid setting for column \"{header}\"." msgid "Invalid setting for column \"{header}\"."
msgstr "" msgstr "Configuração inválida para a coluna “{header}”."
#: pretix/base/modelimport.py:199 #: pretix/base/modelimport.py:199
#, python-brace-format #, python-brace-format
msgid "Could not parse {value} as a yes/no value." msgid "Could not parse {value} as a yes/no value."
msgstr "" msgstr "Não foi possível analisar {value} como um valor sim/não."
#: pretix/base/modelimport.py:222 #: pretix/base/modelimport.py:222
#, python-brace-format #, python-brace-format
msgid "Could not parse {value} as a date and time." msgid "Could not parse {value} as a date and time."
msgstr "" msgstr "Não foi possível analisar {value} como uma data e hora."
#: pretix/base/modelimport.py:232 pretix/control/views/orders.py:1162 #: pretix/base/modelimport.py:232 pretix/control/views/orders.py:1162
#: pretix/control/views/orders.py:1191 pretix/control/views/orders.py:1235 #: pretix/control/views/orders.py:1191 pretix/control/views/orders.py:1235
#: pretix/control/views/orders.py:1270 pretix/control/views/orders.py:1293 #: pretix/control/views/orders.py:1270 pretix/control/views/orders.py:1293
msgid "You entered an invalid number." msgid "You entered an invalid number."
msgstr "" msgstr "Você inseriu um número inválido."
#: pretix/base/modelimport.py:279 pretix/base/modelimport.py:291 #: pretix/base/modelimport.py:279 pretix/base/modelimport.py:291
#, fuzzy #, fuzzy
@@ -3568,7 +3564,7 @@ msgstr "Nenhum evento arquivado encontrado."
#: pretix/base/modelimport.py:281 pretix/base/modelimport.py:293 #: pretix/base/modelimport.py:281 pretix/base/modelimport.py:293
msgctxt "subevent" msgctxt "subevent"
msgid "Multiple matching dates were found." msgid "Multiple matching dates were found."
msgstr "" msgstr "Foram encontradas várias datas correspondentes."
#: pretix/base/modelimport_orders.py:85 #: pretix/base/modelimport_orders.py:85
#, fuzzy #, fuzzy
@@ -3591,7 +3587,7 @@ msgstr "Nenhum evento arquivado encontrado."
#: pretix/base/modelimport_orders.py:130 #: pretix/base/modelimport_orders.py:130
#: pretix/base/modelimport_vouchers.py:196 #: pretix/base/modelimport_vouchers.py:196
msgid "Multiple matching products were found." msgid "Multiple matching products were found."
msgstr "" msgstr "Foram encontrados vários produtos correspondentes."
#: pretix/base/modelimport_orders.py:139 #: pretix/base/modelimport_orders.py:139
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1245 #: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1245
@@ -3611,7 +3607,7 @@ msgstr "Informações da conta alteradas"
#: pretix/base/modelimport_vouchers.py:227 #: pretix/base/modelimport_vouchers.py:227
#: pretix/base/modelimport_vouchers.py:261 #: pretix/base/modelimport_vouchers.py:261
msgid "Multiple matching variations were found." msgid "Multiple matching variations were found."
msgstr "" msgstr "Foram encontradas diversas variações correspondentes."
#: pretix/base/modelimport_orders.py:164 #: pretix/base/modelimport_orders.py:164
#, fuzzy #, fuzzy
@@ -3700,7 +3696,7 @@ msgstr "Tipo de dispositivo"
#: pretix/base/modelimport_orders.py:460 #: pretix/base/modelimport_orders.py:460
msgid "You cannot assign a position secret that already exists." msgid "You cannot assign a position secret that already exists."
msgstr "" msgstr "Não é possível atribuir um segredo de posição que já exista."
#: pretix/base/modelimport_orders.py:491 #: pretix/base/modelimport_orders.py:491
#, fuzzy #, fuzzy
@@ -3788,7 +3784,7 @@ msgstr "Um voucher com esse código já existe."
#: pretix/base/models/vouchers.py:196 pretix/control/views/vouchers.py:120 #: pretix/base/models/vouchers.py:196 pretix/control/views/vouchers.py:120
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:52 #: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:52
msgid "Maximum usages" msgid "Maximum usages"
msgstr "" msgstr "Usos máximos"
#: pretix/base/modelimport_vouchers.py:79 #: pretix/base/modelimport_vouchers.py:79
#, fuzzy #, fuzzy
@@ -3814,20 +3810,22 @@ msgstr "Quantidade máxima por pedido"
#: pretix/base/modelimport_vouchers.py:119 pretix/base/models/vouchers.py:225 #: pretix/base/modelimport_vouchers.py:119 pretix/base/models/vouchers.py:225
#: pretix/control/forms/filter.py:2106 #: pretix/control/forms/filter.py:2106
msgid "Reserve ticket from quota" msgid "Reserve ticket from quota"
msgstr "" msgstr "Reservar bilhete da cota"
#: pretix/base/modelimport_vouchers.py:127 pretix/base/models/vouchers.py:233 #: pretix/base/modelimport_vouchers.py:127 pretix/base/models/vouchers.py:233
msgid "Allow to bypass quota" msgid "Allow to bypass quota"
msgstr "" msgstr "Permitir ignorar a cota"
#: pretix/base/modelimport_vouchers.py:135 pretix/base/models/vouchers.py:239 #: pretix/base/modelimport_vouchers.py:135 pretix/base/models/vouchers.py:239
msgid "Price mode" msgid "Price mode"
msgstr "" msgstr "Modo de preço"
#: pretix/base/modelimport_vouchers.py:150 #: pretix/base/modelimport_vouchers.py:150
#, python-brace-format #, python-brace-format
msgid "Could not parse {value} as a price mode, use one of {options}." msgid "Could not parse {value} as a price mode, use one of {options}."
msgstr "" msgstr ""
"Não foi possível analisar {value} como um modo de preço, use uma das "
"{options}."
#: pretix/base/modelimport_vouchers.py:160 pretix/base/models/vouchers.py:245 #: pretix/base/modelimport_vouchers.py:160 pretix/base/models/vouchers.py:245
msgid "Voucher value" msgid "Voucher value"
@@ -3835,18 +3833,18 @@ msgstr "Valor do voucher"
#: pretix/base/modelimport_vouchers.py:165 #: pretix/base/modelimport_vouchers.py:165
msgid "It is pointless to set a value without a price mode." msgid "It is pointless to set a value without a price mode."
msgstr "" msgstr "Não faz sentido definir um valor sem um modo de preço."
#: pretix/base/modelimport_vouchers.py:237 pretix/base/models/items.py:2081 #: pretix/base/modelimport_vouchers.py:237 pretix/base/models/items.py:2081
#: pretix/base/models/vouchers.py:272 #: pretix/base/models/vouchers.py:272
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:8 #: pretix/control/templates/pretixcontrol/items/quota_edit.html:8
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:15 #: pretix/control/templates/pretixcontrol/items/quota_edit.html:15
msgid "Quota" msgid "Quota"
msgstr "" msgstr "Cota"
#: pretix/base/modelimport_vouchers.py:253 #: pretix/base/modelimport_vouchers.py:253
msgid "You cannot specify a quota if you specified a product." msgid "You cannot specify a quota if you specified a product."
msgstr "" msgstr "Não é possível especificar uma cota se você especificou um produto."
#: pretix/base/modelimport_vouchers.py:282 pretix/base/models/vouchers.py:495 #: pretix/base/modelimport_vouchers.py:282 pretix/base/models/vouchers.py:495
#, fuzzy #, fuzzy
@@ -3869,11 +3867,9 @@ msgid "Seat-specific vouchers can only be used once."
msgstr "Este produto não será vendido após a data indicada." msgstr "Este produto não será vendido após a data indicada."
#: pretix/base/modelimport_vouchers.py:306 pretix/base/models/vouchers.py:519 #: pretix/base/modelimport_vouchers.py:306 pretix/base/models/vouchers.py:519
#, fuzzy, python-brace-format #, python-brace-format
#| msgctxt "subevent"
#| msgid "You need to select a date."
msgid "You need to choose the product \"{prod}\" for this seat." msgid "You need to choose the product \"{prod}\" for this seat."
msgstr "Você precisa selecionar uma data." msgstr "Você precisa escolher o produto “{prod}” para esse assento."
#: pretix/base/modelimport_vouchers.py:318 pretix/base/models/vouchers.py:285 #: pretix/base/modelimport_vouchers.py:318 pretix/base/models/vouchers.py:285
#: pretix/control/templates/pretixcontrol/vouchers/index.html:129 #: pretix/control/templates/pretixcontrol/vouchers/index.html:129
@@ -3884,17 +3880,20 @@ msgstr "Tag"
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:297 #: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:297
msgid "Shows hidden products that match this voucher" msgid "Shows hidden products that match this voucher"
msgstr "" msgstr "Mostra produtos ocultos que correspondem a esse voucher"
#: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:301 #: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:301
msgid "Offer all add-on products for free when redeeming this voucher" msgid "Offer all add-on products for free when redeeming this voucher"
msgstr "" msgstr ""
"Ofereça todos os produtos adicionais gratuitamente ao resgatar este voucher"
#: pretix/base/modelimport_vouchers.py:351 pretix/base/models/vouchers.py:305 #: pretix/base/modelimport_vouchers.py:351 pretix/base/models/vouchers.py:305
msgid "" msgid ""
"Include all bundled products without a designated price when redeeming this " "Include all bundled products without a designated price when redeeming this "
"voucher" "voucher"
msgstr "" msgstr ""
"Inclua todos os produtos incluídos em um pacote sem um preço designado ao "
"resgatar esse voucher"
#: pretix/base/models/auth.py:248 #: pretix/base/models/auth.py:248
msgid "Is active" msgid "Is active"
@@ -3968,22 +3967,28 @@ msgid ""
"and valid for check-in regardless of which date they are purchased for. You " "and valid for check-in regardless of which date they are purchased for. You "
"can limit their validity through the advanced check-in rules, though." "can limit their validity through the advanced check-in rules, though."
msgstr "" msgstr ""
"Se você escolher “todas as datas”, os bilhetes serão considerados parte "
"dessa lista e válidos para o check-in, independentemente da data para a qual "
"foram comprados. No entanto, você pode limitar a validade deles por meio das "
"regras avançadas de check-in."
#: pretix/base/models/checkin.py:65 #: pretix/base/models/checkin.py:65
msgctxt "checkin" msgctxt "checkin"
msgid "Ignore check-ins on this list in statistics" msgid "Ignore check-ins on this list in statistics"
msgstr "" msgstr "Ignorar os check-ins dessa lista nas estatísticas"
#: pretix/base/models/checkin.py:69 #: pretix/base/models/checkin.py:69
msgctxt "checkin" msgctxt "checkin"
msgid "Tickets with a check-in on this list should be considered \"used\"" msgid "Tickets with a check-in on this list should be considered \"used\""
msgstr "" msgstr "Os bilhetes com um check-in nessa lista devem ser considerados “usados”"
#: pretix/base/models/checkin.py:70 #: pretix/base/models/checkin.py:70
msgid "" msgid ""
"This is relevant in various situations, e.g. for deciding if a ticket can " "This is relevant in various situations, e.g. for deciding if a ticket can "
"still be canceled by the customer." "still be canceled by the customer."
msgstr "" msgstr ""
"Isso é relevante em várias situações, por exemplo, para decidir se um "
"bilhete ainda pode ser cancelado pelo cliente."
#: pretix/base/models/checkin.py:74 #: pretix/base/models/checkin.py:74
msgctxt "checkin" msgctxt "checkin"
@@ -4007,6 +4012,8 @@ msgstr ""
#: pretix/base/models/checkin.py:79 #: pretix/base/models/checkin.py:79
msgid "Allow checking in add-on tickets by scanning the main ticket" msgid "Allow checking in add-on tickets by scanning the main ticket"
msgstr "" msgstr ""
"Permitir o check-in de bilhetes adicionais por meio da leitura do bilhete "
"principal"
#: pretix/base/models/checkin.py:81 #: pretix/base/models/checkin.py:81
msgid "" msgid ""
@@ -4014,6 +4021,9 @@ msgid ""
"there is always exactly one matching add-on ticket. Ambiguous scans will be " "there is always exactly one matching add-on ticket. Ambiguous scans will be "
"rejected.." "rejected.."
msgstr "" msgstr ""
"Um escaneamento só será possível se a lista de check-in estiver configurada "
"de forma que sempre haja exatamente um bilhete complementar correspondente. "
"Os escaneamentos ambíguos serão rejeitados..."
#: pretix/base/models/checkin.py:85 pretix/control/navigation.py:640 #: pretix/base/models/checkin.py:85 pretix/control/navigation.py:640
#: pretix/control/templates/pretixcontrol/organizers/gates.html:5 #: pretix/control/templates/pretixcontrol/organizers/gates.html:5
@@ -4028,19 +4038,23 @@ msgid ""
"Does not have any effect for the validation of tickets, only for the " "Does not have any effect for the validation of tickets, only for the "
"automatic configuration of check-in devices." "automatic configuration of check-in devices."
msgstr "" msgstr ""
"Não tem efeito na validação de bilhetes, apenas na configuração automática "
"dos dispositivos de check-in."
#: pretix/base/models/checkin.py:90 #: pretix/base/models/checkin.py:90
msgid "Allow re-entering after an exit scan" msgid "Allow re-entering after an exit scan"
msgstr "" msgstr "Permitir o reingresso após um escaneamento de saída"
#: pretix/base/models/checkin.py:94 #: pretix/base/models/checkin.py:94
msgid "Allow multiple entries per ticket" msgid "Allow multiple entries per ticket"
msgstr "" msgstr "Permitir várias entradas por bilhete"
#: pretix/base/models/checkin.py:95 #: pretix/base/models/checkin.py:95
msgid "" msgid ""
"Use this option to turn off warnings if a ticket is scanned a second time." "Use this option to turn off warnings if a ticket is scanned a second time."
msgstr "" msgstr ""
"Use essa opção para desativar os avisos se um bilhete for escaneado uma "
"segunda vez."
#: pretix/base/models/checkin.py:99 #: pretix/base/models/checkin.py:99
#, fuzzy #, fuzzy
@@ -4056,7 +4070,7 @@ msgstr "País"
#: pretix/base/models/checkin.py:337 #: pretix/base/models/checkin.py:337
msgid "Exit" msgid "Exit"
msgstr "" msgstr "Saída"
#: pretix/base/models/checkin.py:355 #: pretix/base/models/checkin.py:355
#, fuzzy #, fuzzy

View File

@@ -51,8 +51,8 @@ def register_payment_provider(sender, **kwargs):
class PaypalEventLogEntryType(EventLogEntryType): class PaypalEventLogEntryType(EventLogEntryType):
action_type = 'pretix.plugins.paypal.event' action_type = 'pretix.plugins.paypal.event'
def display(self, logentry): def display(self, logentry, data):
event_type = logentry.parsed_data.get('event_type') event_type = data.get('event_type')
text = None text = None
plains = { plains = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'), 'PAYMENT.SALE.COMPLETED': _('Payment completed.'),

View File

@@ -5,6 +5,10 @@
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable"> <div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this product." %}</a></small></p> <p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this product." %}</a></small></p>
</div> </div>
{% elif item.current_unavailability_reason == 'hidden_if_item_available' %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small>{% trans "Not available yet." %}</small></p>
</div>
{% elif item.current_unavailability_reason == 'available_from' or var.current_unavailability_reason == 'available_from' %} {% elif item.current_unavailability_reason == 'available_from' or var.current_unavailability_reason == 'available_from' %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable"> <div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small>{% trans "Not available yet." %}</small></p> <p><small>{% trans "Not available yet." %}</small></p>

View File

@@ -70,7 +70,7 @@ from pretix.base.models import (
) )
from pretix.base.models.event import Event, SubEvent from pretix.base.models.event import Event, SubEvent
from pretix.base.models.items import ( from pretix.base.models.items import (
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation, Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
) )
from pretix.base.services.placeholders import PlaceholderContext from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.quotas import QuotaAvailability
@@ -302,14 +302,14 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
if item.hidden_if_item_available: if item.hidden_if_item_available:
if item.hidden_if_item_available.has_variations: if item.hidden_if_item_available.has_variations:
dependency_available = any( item._dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
for var in item.hidden_if_item_available.available_variations for var in item.hidden_if_item_available.available_variations
) )
else: else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True) q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
dependency_available = q[0] == Quota.AVAILABILITY_OK item._dependency_available = q[0] == Quota.AVAILABILITY_OK
if dependency_available: if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True item._remove = True
continue continue

View File

@@ -38,6 +38,7 @@ var strings = {
'unavailable_available_from': django.pgettext('widget', 'Not yet available'), 'unavailable_available_from': django.pgettext('widget', 'Not yet available'),
'unavailable_available_until': django.pgettext('widget', 'Not available anymore'), 'unavailable_available_until': django.pgettext('widget', 'Not available anymore'),
'unavailable_active': django.pgettext('widget', 'Currently not available'), 'unavailable_active': django.pgettext('widget', 'Currently not available'),
'unavailable_hidden_if_item_available': django.pgettext('widget', 'Not yet available'),
'order_min': django.pgettext('widget', 'minimum amount to order: %s'), 'order_min': django.pgettext('widget', 'minimum amount to order: %s'),
'exit': django.pgettext('widget', 'Close ticket shop'), 'exit': django.pgettext('widget', 'Close ticket shop'),
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'), 'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
@@ -229,7 +230,7 @@ Vue.component('availbox', {
+ ' v-bind:aria-label="label_select_item"' + ' v-bind:aria-label="label_select_item"'
+ '>' + '>'
+ '</label>' + '</label>'
+ '<div :class="count_group_classes" v-else>' + '<div :class="count_group_classes" v-else role="group" v-bind:aria-label="item.name">'
+ '<button v-if="!$root.use_native_spinners" type="button" @click.prevent.stop="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" aria-label="' + strings.quantity_dec + '"><span>-</span></button>' + '<button v-if="!$root.use_native_spinners" type="button" @click.prevent.stop="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" aria-label="' + strings.quantity_dec + '"><span>-</span></button>'
+ '<input type="number" inputmode="numeric" pattern="\d*" class="pretix-widget-item-count-multiple" placeholder="0" min="0"' + '<input type="number" inputmode="numeric" pattern="\d*" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' v-model="amount_selected" :max="order_max" :name="input_name" :id="\'input_\' + input_name"' + ' v-model="amount_selected" :max="order_max" :name="input_name" :id="\'input_\' + input_name"'
@@ -722,7 +723,6 @@ var shared_methods = {
}, },
buy_callback: function (data) { buy_callback: function (data) {
if (data.redirect) { if (data.redirect) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
if (data.cart_id) { if (data.cart_id) {
this.$root.cart_id = data.cart_id; this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30); setCookie(this.$root.cookieName, data.cart_id, 30);
@@ -747,7 +747,7 @@ var shared_methods = {
} }
this.$root.overlay.frame_loading = false; this.$root.overlay.frame_loading = false;
} else { } else {
iframe.src = url; this.$root.overlay.frame_src = url;
} }
} else { } else {
this.async_task_id = data.async_id; this.async_task_id = data.async_id;
@@ -786,9 +786,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) { if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0]; this.$root.overlay.frame_src = redirect_url;
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
}, },
voucher_open: function (voucher) { voucher_open: function (voucher) {
var redirect_url; var redirect_url;
@@ -800,9 +798,7 @@ var shared_methods = {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
if (this.$root.useIframe) { if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0]; this.$root.overlay.frame_src = redirect_url;
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
} else { } else {
window.open(redirect_url); window.open(redirect_url);
} }
@@ -825,9 +821,7 @@ var shared_methods = {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
if (this.$root.useIframe) { if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0]; this.$root.overlay.frame_src = redirect_url;
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
} else { } else {
window.open(redirect_url); window.open(redirect_url);
} }
@@ -852,27 +846,27 @@ var shared_loading_fragment = (
); );
var shared_iframe_fragment = ( var shared_iframe_fragment = (
'<div :class="frameClasses">' '<div :class="frameClasses" role="dialog" aria-modal="true" >'
+ '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">' + '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>' + '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '</div>' + '</div>'
+ '<div class="pretix-widget-frame-inner" ref="frame-container" v-show="$root.frame_shown">' + '<div class="pretix-widget-frame-inner" ref="frame-container" v-show="$root.frame_shown">'
+ '<iframe frameborder="0" width="650" height="650" ref="iframe" @load="iframeLoaded" ' + '<div class="pretix-widget-frame-close"><a href="#" @click.prevent.stop="close" role="button" aria-label="'+strings.close+'">'
+ '<svg height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</a></div>'
+ '<iframe frameborder="0" width="650" height="650" @load="iframeLoaded" '
+ ' :name="$root.parent.widget_id" src="about:blank" v-once' + ' :name="$root.parent.widget_id" src="about:blank" v-once'
+ ' allow="autoplay *; camera *; fullscreen *; payment *"' + ' allow="autoplay *; camera *; fullscreen *; payment *"'
+ ' referrerpolicy="origin">' + ' referrerpolicy="origin">'
+ 'Please enable frames in your browser!' + 'Please enable frames in your browser!'
+ '</iframe>' + '</iframe>'
+ '<div class="pretix-widget-frame-close"><a href="#" @click.prevent.stop="close" role="button" aria-label="'+strings.close+'">'
+ '<svg height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</a></div>'
+ '</div>' + '</div>'
+ '</div>' + '</div>'
); );
var shared_alert_fragment = ( var shared_alert_fragment = (
'<div :class="alertClasses">' '<div :class="alertClasses" role="dialog" aria-modal="true" aria-live="polite">'
+ '<transition name="bounce">' + '<transition name="bounce" @after-enter="focusButton">'
+ '<div class="pretix-widget-alert-box" v-if="$root.error_message">' + '<div class="pretix-widget-alert-box" v-if="$root.error_message">'
+ '<p>{{ $root.error_message }}</p>' + '<p>{{ $root.error_message }}</p>'
+ '<p><button v-if="$root.error_url_after" @click.prevent.stop="errorContinue">' + strings.continue + '</button>' + '<p><button v-if="$root.error_url_after" @click.prevent.stop="errorContinue">' + strings.continue + '</button>'
@@ -907,13 +901,6 @@ Vue.component('pretix-overlay', {
+ shared_lightbox_fragment + shared_lightbox_fragment
+ '</div>' + '</div>'
), ),
mounted: function () {
if (typeof this.$refs.iframe.addEventListenerBase === "function") {
// Workaround for a bug in Cookiebot's magic load event handling (see internal ticket Z#23180085)
console.log("pretix Widget is applying circumvention for a bug in Cookiebot's event handling");
this.$refs.iframe.addEventListenerBase("load", this.iframeLoaded)
}
},
watch: { watch: {
'$root.lightbox': function (newValue, oldValue) { '$root.lightbox': function (newValue, oldValue) {
if (newValue) { if (newValue) {
@@ -972,8 +959,7 @@ Vue.component('pretix-overlay', {
window.open(this.$root.error_url_after); window.open(this.$root.error_url_after);
return; return;
} }
var iframe = this.$refs['frame-container'].children[0]; this.$root.overlay.frame_src = this.$root.error_url_after;
iframe.src = this.$root.error_url_after;
this.$root.frame_loading = true; this.$root.frame_loading = true;
this.$root.error_message = null; this.$root.error_message = null;
this.$root.error_url_after = null; this.$root.error_url_after = null;
@@ -981,15 +967,22 @@ Vue.component('pretix-overlay', {
close: function () { close: function () {
this.$root.frame_shown = false; this.$root.frame_shown = false;
this.$root.parent.frame_dismissed = true; this.$root.parent.frame_dismissed = true;
this.$root.frame_src = "";
this.$root.parent.reload(); this.$root.parent.reload();
this.$root.parent.trigger_close_callback(); this.$root.parent.trigger_close_callback();
}, },
iframeLoaded: function () { iframeLoaded: function () {
if (this.$root.frame_loading) { if (this.$root.frame_loading) {
this.$root.frame_loading = false; this.$root.frame_loading = false;
this.$root.frame_shown = true; if (this.$root.frame_src) {
this.$root.frame_shown = true;
}
} }
} },
focusButton: function () {
this.$el.querySelector(".pretix-widget-alert-box button").focus();
},
} }
}); });
@@ -1744,8 +1737,7 @@ var shared_root_methods = {
} else { } else {
url += '?iframe=1'; url += '?iframe=1';
} }
this.$root.overlay.$children[0].$refs['frame-container'].children[0].src = url; this.$root.overlay.frame_src = url;
this.$root.overlay.frame_loading = true;
} else { } else {
event.target.href = url; event.target.href = url;
return; return;
@@ -1908,9 +1900,7 @@ var shared_root_methods = {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
if (this.$root.useIframe) { if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0]; this.$root.overlay.frame_src = redirect_url;
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
} else { } else {
window.open(redirect_url); window.open(redirect_url);
} }
@@ -1934,9 +1924,7 @@ var shared_root_methods = {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
if (this.$root.useIframe) { if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0]; this.$root.overlay.frame_src = redirect_url;
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
} else { } else {
window.open(redirect_url); window.open(redirect_url);
} }
@@ -2073,9 +2061,41 @@ var create_overlay = function (app) {
error_url_after_new_tab: true, error_url_after_new_tab: true,
error_message: null, error_message: null,
lightbox: null, lightbox: null,
prevActiveElement: null,
} }
}, },
props: {
frame_src: String,
},
methods: { methods: {
},
watch: {
frame_src: function (newValue, oldValue) {
// show loading spinner only when previously no frame_src was set
if (newValue && !oldValue) {
this.frame_loading = true;
}
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
this.$el.querySelector("iframe").src = newValue || "about:blank";
},
frame_shown: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
var btn = this.$el?.querySelector(".pretix-widget-frame-close a");
this.$nextTick(function () {
btn?.focus();
});
} else {
this.prevActiveElement?.focus();
}
},
error_message: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
} else {
this.prevActiveElement?.focus();
}
},
} }
}); });
app.$root.overlay = framechild; app.$root.overlay = framechild;

View File

@@ -323,6 +323,7 @@ TEST_ITEM_RES = {
"max_per_order": None, "max_per_order": None,
"hidden_if_available": None, "hidden_if_available": None,
"hidden_if_item_available": None, "hidden_if_item_available": None,
"hidden_if_item_available_mode": "hide",
"checkin_attention": False, "checkin_attention": False,
"checkin_text": None, "checkin_text": None,
"has_variations": False, "has_variations": False,