mirror of
https://github.com/pretix/pretix.git
synced 2026-06-18 02:26:17 +00:00
Compare commits
192 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12ba49e263 | |||
| e37b032d4e | |||
| 3c3b5529bf | |||
| 87b3e0c417 | |||
| d3fd031639 | |||
| 9253327334 | |||
| 080b9cacaf | |||
| 9c2cc02df1 | |||
| fceae0a2fe | |||
| 9fc3fdf751 | |||
| 04f79b7014 | |||
| 9d0b9387e6 | |||
| b25e6f598d | |||
| e8e2648f7e | |||
| e0fac42225 | |||
| 3e9bc7675b | |||
| 1541033467 | |||
| 6b8c3ef15c | |||
| 135e07c183 | |||
| fe97915b36 | |||
| 233281cea4 | |||
| 0300a44634 | |||
| 449d930565 | |||
| 49f49bd8a6 | |||
| e896704fe0 | |||
| cfee402a27 | |||
| f8878e53a3 | |||
| fd6a342bc6 | |||
| 865433276e | |||
| f616f64f47 | |||
| 26550887b7 | |||
| 0f3de911b8 | |||
| b648390dbf | |||
| 50fec0b31c | |||
| e44af04e43 | |||
| 276c3177f5 | |||
| 27ac004a0b | |||
| 6d517d4e8d | |||
| d9c3deda8a | |||
| fe6add618a | |||
| 3615a52cc4 | |||
| e3ae3b08bd | |||
| 959e926a67 | |||
| 876ddf1321 | |||
| 005b1d54d3 | |||
| 2066471086 | |||
| a25bca7471 | |||
| da43984ad2 | |||
| 7cce1c9219 | |||
| cb9c4466f9 | |||
| 3398cda74b | |||
| e5c8f19984 | |||
| 5027f6dd59 | |||
| 787db18d72 | |||
| aadce7be00 | |||
| 26f296bc11 | |||
| 6ae80cdd4b | |||
| cb3956c994 | |||
| b9f350bf3a | |||
| ab447bb85f | |||
| bf33a42ae8 | |||
| 081f975ff9 | |||
| eab7d81a51 | |||
| b2dce51a24 | |||
| 5bd660a913 | |||
| 8e9cdd7548 | |||
| d6592cbb93 | |||
| 0e3ccae5d4 | |||
| 034b46d218 | |||
| a3f120198d | |||
| fa5f3bb15a | |||
| 5120b312b6 | |||
| 09064844b2 | |||
| 1a60b3a712 | |||
| 6216f0d7df | |||
| 380b55e699 | |||
| 6e67bb5045 | |||
| 1463ee9227 | |||
| 3b49e77722 | |||
| ceed07af94 | |||
| 802c03f8f3 | |||
| 9962d8a3be | |||
| 028a41f3e4 | |||
| 6d8a9854f9 | |||
| 861e14bb16 | |||
| 7a080c0820 | |||
| 2dbdb91066 | |||
| b8efb8f61d | |||
| 5f0cc4cc59 | |||
| d3bb1f3190 | |||
| 69a215feff | |||
| 435dd5ebaf | |||
| 015d74f7ae | |||
| 5c9a069d77 | |||
| 5866cf94ee | |||
| fa15ba4435 | |||
| e982f04d59 | |||
| ced00266dc | |||
| b534c125db | |||
| 769e1312d4 | |||
| 3d53c03906 | |||
| 59d1d2cb16 | |||
| 7e45837295 | |||
| fd9ed15065 | |||
| 2df3d9206b | |||
| fbd8bbbeaa | |||
| 1c305e4b30 | |||
| ea114b4f64 | |||
| 0342613635 | |||
| 743c4b796b | |||
| 8a7f54795e | |||
| cb464ad597 | |||
| 119cc50897 | |||
| 61f9cf13b4 | |||
| f24429a7c5 | |||
| 29ed07ccce | |||
| dd0cd7ab0b | |||
| d7df906995 | |||
| 839f4b4657 | |||
| 74f7e1f61c | |||
| 47919afab0 | |||
| 819daa99f7 | |||
| 8512e79d68 | |||
| 52672ae25b | |||
| ad752dc617 | |||
| 43c6c33bd8 | |||
| 88c9f8c047 | |||
| 2d2663f15f | |||
| ae6014708b | |||
| d1686df07c | |||
| 4d60d7bfbc | |||
| c0b93fedc5 | |||
| 2eaa6c3069 | |||
| db982c9ef4 | |||
| f9f6ee94ae | |||
| 99c257d392 | |||
| e2cb83ce28 | |||
| d7b7d3cc5f | |||
| 721ac8a500 | |||
| 5796cfe03f | |||
| 63f1c4f793 | |||
| 47f409171d | |||
| 27fcdff17f | |||
| a38a96f186 | |||
| 700ea77e39 | |||
| 06104ff483 | |||
| fb5697a27b | |||
| 9a9ad6d6d1 | |||
| a05845790e | |||
| a0830dd033 | |||
| dba2529f6b | |||
| 9c0ea8f179 | |||
| 1f0501a647 | |||
| d2e6446238 | |||
| d519fcfe0d | |||
| c7226303be | |||
| 9406e941bc | |||
| 919e598f8a | |||
| 672692d578 | |||
| 9429dc7e91 | |||
| 5c8dbd99dd | |||
| cfbb8310f0 | |||
| d37d9a861c | |||
| 43a9cf29b2 | |||
| 047ad438a7 | |||
| ec8d921fcf | |||
| 39e6ef4365 | |||
| 4d8b032591 | |||
| e8193e408b | |||
| 6723d8c07c | |||
| c30134f36c | |||
| 0617fc04ec | |||
| e90b54280a | |||
| 39ea2889ba | |||
| 76230bd37b | |||
| 86b8c5e90f | |||
| 7a2027c61b | |||
| 55de5ef45b | |||
| 39e6954828 | |||
| 7849d98672 | |||
| c325164059 | |||
| 8fc19c62dd | |||
| 20feaebbbd | |||
| 4587a9e630 | |||
| 9fe0e6eb67 | |||
| bab7e54f35 | |||
| 352efa40e7 | |||
| 50da7d4261 | |||
| 53cc59d41d | |||
| 9879e99c59 | |||
| dc49d5bcf7 | |||
| d4460045b4 |
@@ -60,6 +60,9 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.event.added``
|
||||
* ``pretix.event.changed``
|
||||
* ``pretix.event.deleted``
|
||||
* ``pretix.giftcards.created``
|
||||
* ``pretix.giftcards.modified``
|
||||
* ``pretix.giftcards.transaction.*``
|
||||
* ``pretix.voucher.added``
|
||||
* ``pretix.voucher.changed``
|
||||
* ``pretix.voucher.deleted``
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0
|
||||
sphinxcontrib-httpdomain~=1.8.1
|
||||
sphinxcontrib-httpdomain~=2.0.0
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-e ../
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0
|
||||
sphinxcontrib-httpdomain~=1.8.1
|
||||
sphinxcontrib-httpdomain~=2.0.0
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
|
||||
+6
-6
@@ -33,7 +33,7 @@ dependencies = [
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.19.*",
|
||||
"css-inline==0.20.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==4.2.*,>=4.2.26",
|
||||
@@ -65,7 +65,7 @@ dependencies = [
|
||||
"kombu==5.6.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.10.1", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.3.*",
|
||||
@@ -73,7 +73,7 @@ dependencies = [
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.10.*",
|
||||
"PyJWT==2.11.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.1.*",
|
||||
"pretix-plugin-build",
|
||||
@@ -92,7 +92,7 @@ dependencies = [
|
||||
"redis==7.1.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.51.*",
|
||||
"sentry-sdk==2.53.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -110,10 +110,10 @@ dev = [
|
||||
"aiohttp==3.13.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.33.*",
|
||||
"fakeredis==2.34.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==7.0.*",
|
||||
"isort==8.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=0.24",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "2026.2.0.dev0"
|
||||
__version__ = "2026.3.0.dev0"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# 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/>.
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import Counter, defaultdict
|
||||
@@ -1215,6 +1216,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The given payment provider is not known.')
|
||||
return pp
|
||||
|
||||
def validate_payment_info(self, info):
|
||||
if info:
|
||||
try:
|
||||
obj = json.loads(info)
|
||||
except ValueError:
|
||||
raise ValidationError('payment_info must be valid JSON.')
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
# only objects are allowed
|
||||
raise ValidationError('payment_info must be a JSON object.')
|
||||
return info
|
||||
|
||||
def validate_expires(self, expires):
|
||||
if expires < now():
|
||||
raise ValidationError('Expiration date must be in the future.')
|
||||
|
||||
@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
||||
def _send_invite(self, instance):
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
_('Account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': self,
|
||||
'organizer': self.context['organizer'].name,
|
||||
'team': instance.team.name,
|
||||
|
||||
@@ -188,11 +188,15 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
clist = self.get_object()
|
||||
if serializer.validated_data.get('nonce'):
|
||||
if kwargs.get('position'):
|
||||
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
|
||||
prev = kwargs['position'].all_checkins.filter(
|
||||
nonce=serializer.validated_data['nonce'],
|
||||
successful=False
|
||||
).first()
|
||||
else:
|
||||
prev = clist.checkins.filter(
|
||||
nonce=serializer.validated_data['nonce'],
|
||||
raw_barcode=serializer.validated_data['raw_barcode'],
|
||||
successful=False
|
||||
).first()
|
||||
if prev:
|
||||
# Ignore because nonce is already handled
|
||||
|
||||
@@ -249,12 +249,24 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
def perform_create(self, serializer):
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(issuer=self.request.organizer)
|
||||
inst.transactions.create(value=value, acceptor=self.request.organizer)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
action='pretix.giftcards.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
inst.transactions.create(value=value, acceptor=self.request.organizer)
|
||||
inst.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(
|
||||
self.request.data,
|
||||
{
|
||||
'id': inst.pk,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
@@ -269,7 +281,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.modified',
|
||||
action='pretix.giftcards.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
@@ -282,10 +294,14 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
diff = value - old_value
|
||||
inst.transactions.create(value=diff, acceptor=self.request.organizer)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
data={
|
||||
'value': diff,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
return inst
|
||||
@@ -309,10 +325,15 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': value, 'text': text}
|
||||
data={
|
||||
'value': value,
|
||||
'text': text,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -174,6 +174,38 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
giftcard = logentry.content_object
|
||||
if not giftcard:
|
||||
return None
|
||||
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'issuer_id': logentry.organizer_id,
|
||||
'issuer_slug': logentry.organizer.slug,
|
||||
'giftcard': giftcard.pk,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
giftcard = logentry.content_object
|
||||
if not giftcard:
|
||||
return None
|
||||
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'issuer_id': logentry.organizer_id,
|
||||
'issuer_slug': logentry.organizer.slug,
|
||||
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
|
||||
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
|
||||
'giftcard': giftcard.pk,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
@@ -433,6 +465,18 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.customer.anonymized',
|
||||
_('Customer account anonymized'),
|
||||
),
|
||||
ParametrizedGiftcardWebhookEvent(
|
||||
'pretix.giftcards.created',
|
||||
_('Gift card added'),
|
||||
),
|
||||
ParametrizedGiftcardWebhookEvent(
|
||||
'pretix.giftcards.modified',
|
||||
_('Gift card modified'),
|
||||
),
|
||||
ParametrizedGiftcardTransactionWebhookEvent(
|
||||
'pretix.giftcards.transaction.*',
|
||||
_('Gift card used in transaction'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -216,7 +216,10 @@ class OutboundSyncProvider:
|
||||
|
||||
try:
|
||||
mapped_objects = self.sync_order(sq.order)
|
||||
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
|
||||
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list]
|
||||
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
|
||||
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
|
||||
if should_write_logentry:
|
||||
sq.order.log_action("pretix.event.order.data_sync.success", {
|
||||
"provider": self.identifier,
|
||||
"objects": {
|
||||
@@ -237,7 +240,7 @@ class OutboundSyncProvider:
|
||||
sq.set_sync_error("exceeded", e.messages, e.full_message)
|
||||
else:
|
||||
logger.info(
|
||||
f"Could not sync order {sq.order.code} to {type(self).__name__} "
|
||||
f"Could not sync order {sq.order.code} to {sq.sync_provider} "
|
||||
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
|
||||
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -141,6 +141,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
apply_format_map = not isinstance(plain_body, FormattedString)
|
||||
body_md = self.compile_markdown(plain_body, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
@@ -149,12 +150,13 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
if apply_format_map:
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Voucher budget usage'),
|
||||
_('Voucher tag'),
|
||||
_('Pseudonymization ID'),
|
||||
_('Ticket secret'),
|
||||
_('Seat ID'),
|
||||
@@ -769,6 +770,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
op.state_for_address or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.voucher_budget_use if op.voucher_budget_use else '',
|
||||
op.voucher.tag if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
op.secret,
|
||||
]
|
||||
|
||||
@@ -890,18 +890,18 @@ class BaseQuestionsForm(forms.Form):
|
||||
if not help_text:
|
||||
if q.valid_date_min and q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date between {min} and {max}.',
|
||||
_('Please enter a date between {min} and {max}.'),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date no earlier than {min}.',
|
||||
_('Please enter a date no earlier than {min}.'),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date no later than {max}.',
|
||||
_('Please enter a date no later than {max}.'),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
@@ -939,18 +939,18 @@ class BaseQuestionsForm(forms.Form):
|
||||
if not help_text:
|
||||
if q.valid_datetime_min and q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time between {min} and {max}.',
|
||||
_('Please enter a date and time between {min} and {max}.'),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time no earlier than {min}.',
|
||||
_('Please enter a date and time no earlier than {min}.'),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time no later than {max}.',
|
||||
_('Please enter a date and time no later than {max}.'),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ from django.utils.html import escape
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.helpers.format import PlainHtmlAlternativeString
|
||||
|
||||
|
||||
def replace_arabic_numbers(inp):
|
||||
if not isinstance(inp, str):
|
||||
@@ -61,11 +63,18 @@ def replace_arabic_numbers(inp):
|
||||
return inp.translate(table)
|
||||
|
||||
|
||||
def format_placeholder_help_text(placeholder_name, sample_value):
|
||||
if isinstance(sample_value, PlainHtmlAlternativeString):
|
||||
sample_value = sample_value.plain
|
||||
title = (_("Sample: %s") % sample_value) if sample_value else ""
|
||||
return ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(title), escape(placeholder_name)))
|
||||
|
||||
|
||||
def format_placeholders_help_text(placeholders, event=None):
|
||||
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||
placeholders.sort(key=lambda x: x[0])
|
||||
phs = [
|
||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
|
||||
format_placeholder_help_text(k, v)
|
||||
for k, v in placeholders
|
||||
]
|
||||
return _('Available placeholders: {list}').format(
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
from typing import Optional
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
@@ -106,6 +107,22 @@ class TransmissionType:
|
||||
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
|
||||
return transmission_info
|
||||
|
||||
def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
|
||||
form_data = self.transmission_info_to_form_data(transmission_info)
|
||||
data = []
|
||||
visible_field_keys = self.invoice_address_form_fields_visible(country, is_business)
|
||||
for k, f in self.invoice_address_form_fields.items():
|
||||
if k not in visible_field_keys:
|
||||
continue
|
||||
v = form_data.get(k)
|
||||
if v is True:
|
||||
v = _("Yes")
|
||||
elif v is False:
|
||||
v = _("No")
|
||||
if v:
|
||||
data.append((f.label, v))
|
||||
return data
|
||||
|
||||
def pdf_watermark(self) -> Optional[str]:
|
||||
"""
|
||||
Return a watermark that should be rendered across the PDF file.
|
||||
|
||||
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
|
||||
|
||||
class PriceModeColumn(ImportColumn):
|
||||
identifier = 'price_mode'
|
||||
verbose_name = gettext_lazy('Price mode')
|
||||
verbose_name = gettext_lazy('Price effect')
|
||||
default_value = None
|
||||
initial = 'static:none'
|
||||
|
||||
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
|
||||
elif value in reverse:
|
||||
return reverse[value]
|
||||
else:
|
||||
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
|
||||
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
|
||||
value=value, options=', '.join(d.keys())
|
||||
))
|
||||
|
||||
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
|
||||
def clean(self, value, previous_values):
|
||||
value = super().clean(value, previous_values)
|
||||
if value and previous_values.get("price_mode") == "none":
|
||||
raise ValidationError(_("It is pointless to set a value without a price mode."))
|
||||
raise ValidationError(_("It is pointless to set a value without a price effect."))
|
||||
return value
|
||||
|
||||
def assign(self, value, obj: Voucher, **kwargs):
|
||||
|
||||
@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
{
|
||||
'user': self,
|
||||
'messages': msg,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
'url': build_absolute_uri('control:user.settings'),
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
'user': self,
|
||||
'reason': msg,
|
||||
'code': code,
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
mail(
|
||||
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': self,
|
||||
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||
|
||||
@@ -130,6 +130,8 @@ class LoggingMixin:
|
||||
organizer_id = self.event.organizer_id
|
||||
elif hasattr(self, 'organizer_id'):
|
||||
organizer_id = self.organizer_id
|
||||
elif hasattr(self, 'issuer_id'):
|
||||
organizer_id = self.issuer_id
|
||||
|
||||
if user and not user.is_authenticated:
|
||||
user = None
|
||||
|
||||
@@ -40,6 +40,7 @@ from i18nfield.fields import I18nCharField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.giftcards import GiftCardTransaction
|
||||
@@ -164,6 +165,28 @@ class Customer(LoggedModel):
|
||||
self.attendee_profiles.all().delete()
|
||||
self.invoice_addresses.all().delete()
|
||||
|
||||
def send_security_notice(self, message, email=None):
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
try:
|
||||
with language(self.locale):
|
||||
mail(
|
||||
email or self.email,
|
||||
self.organizer.settings.mail_subject_customer_security_notice,
|
||||
self.organizer.settings.mail_text_customer_security_notice,
|
||||
{
|
||||
**self.get_email_context(),
|
||||
'message': str(message),
|
||||
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
|
||||
},
|
||||
customer=self,
|
||||
organizer=self.organizer,
|
||||
locale=self.locale
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@scopes_disabled()
|
||||
def assign_identifier(self):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789')
|
||||
|
||||
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
|
||||
|
||||
def set_sync_error(self, failure_mode, messages, full_message):
|
||||
logger.exception(
|
||||
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
|
||||
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})"
|
||||
)
|
||||
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
|
||||
"provider": self.sync_provider,
|
||||
|
||||
@@ -87,7 +87,7 @@ from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.format import FormattedString, format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1178,7 +1178,8 @@ class Order(LockModel, LoggedModel):
|
||||
recipient = position.attendee_email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
@@ -2907,7 +2908,8 @@ class OrderPosition(AbstractPosition):
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
@@ -3507,18 +3509,10 @@ class InvoiceAddress(models.Model):
|
||||
def describe_transmission(self):
|
||||
from pretix.base.invoicing.transmission import transmission_types
|
||||
data = []
|
||||
|
||||
t, __ = transmission_types.get(identifier=self.transmission_type)
|
||||
data.append((_("Transmission type"), t.public_name))
|
||||
form_data = t.transmission_info_to_form_data(self.transmission_info or {})
|
||||
for k, f in t.invoice_address_form_fields.items():
|
||||
v = form_data.get(k)
|
||||
if v is True:
|
||||
v = _("Yes")
|
||||
elif v is False:
|
||||
v = _("No")
|
||||
if v:
|
||||
data.append((f.label, v))
|
||||
if self.transmission_info:
|
||||
data += t.describe_info(self.transmission_info, self.country, self.is_business)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
|
||||
)
|
||||
)
|
||||
price_mode = models.CharField(
|
||||
verbose_name=_("Price mode"),
|
||||
verbose_name=_("Price effect"),
|
||||
max_length=100,
|
||||
choices=PRICE_MODES,
|
||||
default='none'
|
||||
|
||||
@@ -1646,6 +1646,14 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
payment.confirm(send_mail=not is_early_special_case, generate_invoice=not is_early_special_case)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.payment',
|
||||
data={
|
||||
'value': trans.value,
|
||||
'acceptor_id': self.event.organizer.id,
|
||||
'acceptor_slug': self.event.organizer.slug
|
||||
}
|
||||
)
|
||||
except PaymentException as e:
|
||||
payment.fail(info={'error': str(e)})
|
||||
raise e
|
||||
@@ -1670,6 +1678,15 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
refund.done()
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.refund',
|
||||
data={
|
||||
'value': refund.amount,
|
||||
'acceptor_id': self.event.organizer.id,
|
||||
'acceptor_slug': self.event.organizer.slug,
|
||||
'text': refund.comment,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||
|
||||
@@ -65,7 +65,7 @@ def get_all_plugins(*, event=None, organizer=None) -> List[type]:
|
||||
if app.name in settings.PRETIX_PLUGINS_EXCLUDE:
|
||||
continue
|
||||
|
||||
level = getattr(app, "level", PLUGIN_LEVEL_EVENT)
|
||||
level = getattr(meta, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_EVENT:
|
||||
if event and hasattr(app, 'is_available'):
|
||||
if not app.is_available(event):
|
||||
|
||||
@@ -334,7 +334,8 @@ def _check_position_constraints(
|
||||
raise CartPositionError(error_messages['voucher_invalid_subevent'])
|
||||
|
||||
# Voucher expired
|
||||
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
|
||||
# (checked using real_now_dt as vouchers influence quota calculations)
|
||||
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
|
||||
raise CartPositionError(error_messages['voucher_expired'])
|
||||
|
||||
# Subevent has been disabled
|
||||
|
||||
@@ -81,7 +81,9 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.format import (
|
||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.ical import get_private_icals
|
||||
@@ -218,6 +220,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if email == INVALID_ADDRESS:
|
||||
return
|
||||
|
||||
if isinstance(template, FormattedString):
|
||||
raise TypeError("Cannot pass an already formatted body template")
|
||||
|
||||
if no_order_links and not plain_text_only:
|
||||
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
|
||||
|
||||
@@ -251,23 +256,28 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if event and attach_tickets and not event.settings.mail_attach_tickets:
|
||||
attach_tickets = False
|
||||
|
||||
with language(locale):
|
||||
with language(locale), override(timezone):
|
||||
if isinstance(context, dict) and order:
|
||||
_autoextend_context(context, order)
|
||||
|
||||
# Build raw content
|
||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
if settings_holder:
|
||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||
else:
|
||||
signature = ""
|
||||
|
||||
# Build full plain-text body
|
||||
if not isinstance(content_plain, FormattedString):
|
||||
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
else:
|
||||
body_plain = content_plain
|
||||
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
|
||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
|
||||
# Build subject
|
||||
subject = str(subject).format_map(TolerantDict(context))
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
|
||||
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
|
||||
if settings_holder:
|
||||
subject = prefix_subject(settings_holder, subject)
|
||||
@@ -286,26 +296,24 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
else:
|
||||
renderer = ClassicMailRenderer(None, organizer)
|
||||
|
||||
with override(timezone):
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
|
||||
m = OutgoingMail.objects.create(
|
||||
organizer=organizer,
|
||||
@@ -321,7 +329,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
body_plain=body_plain,
|
||||
body_html=body_html,
|
||||
sender=sender,
|
||||
headers=headers,
|
||||
headers=headers or {},
|
||||
should_attach_tickets=attach_tickets,
|
||||
should_attach_ical=attach_ical,
|
||||
should_attach_other_files=attach_other_files or [],
|
||||
@@ -381,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
# mail_send_task(self, *, outgoing_mail)
|
||||
with scopes_disabled():
|
||||
mail_send(**kwargs)
|
||||
return
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Unknown arguments")
|
||||
|
||||
@@ -435,15 +443,24 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
|
||||
# why (probably some race condition with ticket cache invalidation?), so retry later.
|
||||
try:
|
||||
self.retry(max_retries=5, countdown=60)
|
||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
|
||||
retry_after = 60
|
||||
outgoing_mail.error = "Tickets not ready"
|
||||
outgoing_mail.error_detail = str(e)
|
||||
outgoing_mail.sent = now()
|
||||
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
|
||||
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
|
||||
"actual_attachments"])
|
||||
self.retry(max_retries=5, countdown=retry_after)
|
||||
except MaxRetriesExceededError:
|
||||
# Well then, something is really wrong, let's send it without attachment before we
|
||||
# don't send at all
|
||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
|
||||
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
|
||||
pass
|
||||
|
||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||
@@ -519,6 +536,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
expected_recipients = {e.lower() for e in expected_recipients if e}
|
||||
if any(t in expected_recipients for t in outgoing_mail.to):
|
||||
invoices_to_mark_transmitted.append(inv)
|
||||
|
||||
@@ -763,7 +781,7 @@ def mail_send(to: List[str], subject: str, body: str, html: Optional[str], sende
|
||||
body_plain=body,
|
||||
body_html=html,
|
||||
sender=sender,
|
||||
headers=headers,
|
||||
headers=headers or {},
|
||||
should_attach_tickets=attach_tickets,
|
||||
should_attach_ical=attach_ical,
|
||||
should_attach_other_files=attach_other_files or [],
|
||||
@@ -789,7 +807,12 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
body = tpl.render(context)
|
||||
context = {
|
||||
# Known bug, should behave differently for plain and HTML but we'll fix after security release
|
||||
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
}
|
||||
body = FormattedString(tpl.render(context))
|
||||
return body
|
||||
|
||||
|
||||
@@ -935,7 +958,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
if signature:
|
||||
signature = signature.format(event=event.name if event else '')
|
||||
signature = format_map(signature, {"event": event.name if event else ''})
|
||||
body_plain += signature
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
@@ -947,7 +970,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += _(
|
||||
"You can view your order details at the following URL:\n{orderurl}."
|
||||
).replace("\n", "\r\n").format(
|
||||
event=event.name, orderurl=build_absolute_uri(
|
||||
orderurl=build_absolute_uri(
|
||||
order.event, 'presale:event.order.position', kwargs={
|
||||
'order': order.code,
|
||||
'secret': position.web_secret,
|
||||
|
||||
@@ -247,6 +247,16 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
||||
for gc in position.issued_gift_cards.all():
|
||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'value': position.price,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
for m in position.granted_memberships.all():
|
||||
@@ -548,6 +558,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
)
|
||||
else:
|
||||
gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=user,
|
||||
data={
|
||||
'value': -position.price,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
for m in position.granted_memberships.all():
|
||||
m.canceled = True
|
||||
@@ -2434,6 +2453,16 @@ class OrderChangeManager:
|
||||
))
|
||||
else:
|
||||
gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.user,
|
||||
auth=self.auth,
|
||||
data={
|
||||
'value': -position.price,
|
||||
'acceptor_id': self.order.event.organizer.id,
|
||||
'acceptor_slug': self.order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
for m in position.granted_memberships.with_usages().all():
|
||||
m.canceled = True
|
||||
@@ -2451,6 +2480,16 @@ class OrderChangeManager:
|
||||
))
|
||||
else:
|
||||
gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.user,
|
||||
auth=self.auth,
|
||||
data={
|
||||
'value': -opa.position.price,
|
||||
'acceptor_id': self.order.event.organizer.id,
|
||||
'acceptor_slug': self.order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
for m in opa.granted_memberships.with_usages().all():
|
||||
m.canceled = True
|
||||
@@ -3119,7 +3158,10 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
|
||||
customer=order.customer,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', data={})
|
||||
giftcard.log_action(
|
||||
action='pretix.giftcards.created',
|
||||
data={}
|
||||
)
|
||||
r = order.refunds.create(
|
||||
order=order,
|
||||
payment=None,
|
||||
@@ -3406,7 +3448,18 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
||||
currency=sender.currency, issued_in=p, testmode=order.testmode,
|
||||
expires=sender.organizer.default_gift_card_expiry,
|
||||
)
|
||||
gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.created',
|
||||
)
|
||||
trans = gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
|
||||
gc.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
data={
|
||||
'value': trans.value,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
any_giftcards = True
|
||||
p.secret = gc.secret
|
||||
p.save(update_fields=['secret'])
|
||||
|
||||
@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
|
||||
_('Data shredding completed'),
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
|
||||
@@ -2948,6 +2948,28 @@ If you did not request a new password, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your {organizer} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_customer_security_notice': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("Changes to your account at {organizer}")),
|
||||
},
|
||||
'mail_text_customer_security_notice': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
|
||||
|
||||
the following change has been made to your account at {organizer}:
|
||||
|
||||
{message}
|
||||
|
||||
You can review and change your account settings here:
|
||||
|
||||
{url}
|
||||
|
||||
If this change was not performed by you, please contact us immediately.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your {organizer} team""")) # noqa: W291
|
||||
},
|
||||
'smtp_use_custom': {
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<h1>{% trans "Not found" %}</h1>
|
||||
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
|
||||
<p>{{ exception }}</p>
|
||||
<p class="links">
|
||||
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
|
||||
</p>
|
||||
{% if request.user.is_staff and not staff_session %}
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||
<p>
|
||||
|
||||
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix 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 django import template
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("anon_email")
|
||||
def anon_email(value):
|
||||
"""Replaces @ with [at] and . with [dot] for anonymization."""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
value = value.replace("@", "[at]").replace(".", "[dot]")
|
||||
return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value]))
|
||||
@@ -95,6 +95,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
|
||||
'csp_report',
|
||||
'widget',
|
||||
'lead',
|
||||
'scheduling',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import pycountry
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext, pgettext, pgettext_lazy
|
||||
@@ -29,6 +30,7 @@ from django_scopes import scope
|
||||
from pretix.base.addressvalidation import (
|
||||
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoicing.transmission import get_transmission_types
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.base.models.tax import VAT_ID_COUNTRIES
|
||||
@@ -89,7 +91,7 @@ def _info(cc):
|
||||
}
|
||||
|
||||
|
||||
def address_form(request):
|
||||
def _address_form(request):
|
||||
cc = request.GET.get("country", "DE")
|
||||
info = _info(cc)
|
||||
|
||||
@@ -157,4 +159,15 @@ def address_form(request):
|
||||
# The help text explains that it is optional, so we want to hide that if it is required
|
||||
info["vat_id"]["helptext_visible"] = False
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def address_form(request):
|
||||
locale = request.GET.get('locale')
|
||||
if locale in dict(settings.LANGUAGES):
|
||||
with language(locale):
|
||||
info = _address_form(request)
|
||||
else:
|
||||
info = _address_form(request)
|
||||
|
||||
return JsonResponse(info)
|
||||
|
||||
@@ -2820,8 +2820,8 @@ class DeviceFilterForm(FilterForm):
|
||||
|
||||
class OutgoingMailFilterForm(FilterForm):
|
||||
orders = {
|
||||
'date': 'd',
|
||||
'-date': '-d',
|
||||
'date': 'created',
|
||||
'-date': '-created',
|
||||
}
|
||||
query = forms.CharField(
|
||||
label=_('Search email address or subject'),
|
||||
|
||||
@@ -635,6 +635,16 @@ class MailSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=I18nMarkdownTextarea,
|
||||
)
|
||||
mail_subject_customer_security_notice = I18nFormField(
|
||||
label=_("Subject"),
|
||||
required=False,
|
||||
widget=I18nTextInput,
|
||||
)
|
||||
mail_text_customer_security_notice = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nMarkdownTextarea,
|
||||
)
|
||||
|
||||
base_context = {
|
||||
'mail_text_customer_registration': ['customer', 'url'],
|
||||
@@ -643,6 +653,8 @@ class MailSettingsForm(SettingsForm):
|
||||
'mail_subject_customer_email_change': ['customer', 'url'],
|
||||
'mail_text_customer_reset': ['customer', 'url'],
|
||||
'mail_subject_customer_reset': ['customer', 'url'],
|
||||
'mail_text_customer_security_notice': ['customer', 'url', 'message'],
|
||||
'mail_subject_customer_security_notice': ['customer', 'url', 'message'],
|
||||
}
|
||||
|
||||
def _get_sample_context(self, base_parameters):
|
||||
@@ -656,6 +668,9 @@ class MailSettingsForm(SettingsForm):
|
||||
'presale:organizer.customer.activate'
|
||||
) + '?token=' + get_random_string(30)
|
||||
|
||||
if 'message' in base_parameters:
|
||||
placeholders['message'] = _('Your password has been changed.')
|
||||
|
||||
if 'customer' in base_parameters:
|
||||
placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe')
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
|
||||
|
||||
@@ -19,17 +19,44 @@
|
||||
# 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 django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.questions import (
|
||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
|
||||
)
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
|
||||
class WaitingListEntryTransferForm(I18nModelForm):
|
||||
class WaitingListEntryEditForm(I18nModelForm):
|
||||
itemvar = forms.ChoiceField(
|
||||
error_messages={
|
||||
'invalid_choice': _("Select a valid choice.")
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.get('instance', None)
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
choices = []
|
||||
if self.instance and self.instance.pk and 'itemvar' not in initial:
|
||||
if self.instance.variation is not None:
|
||||
initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}'
|
||||
if self.instance.variation.active is False:
|
||||
choices.append((initial['itemvar'], str(self.instance.variation)))
|
||||
else:
|
||||
initial['itemvar'] = self.instance.item.pk
|
||||
if self.instance.item.active is False:
|
||||
choices.append((initial['itemvar'], str(self.instance)))
|
||||
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.event.has_subevents:
|
||||
@@ -45,12 +72,73 @@ class WaitingListEntryTransferForm(I18nModelForm):
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
if self.event.settings.waiting_list_names_asked:
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=self.event.settings.waiting_list_names_required,
|
||||
scheme=self.event.organizer.settings.name_scheme,
|
||||
titles=self.event.organizer.settings.name_scheme_titles,
|
||||
label=_('Name'),
|
||||
)
|
||||
else:
|
||||
del self.fields['name_parts']
|
||||
|
||||
if not self.event.settings.waiting_list_phones_asked:
|
||||
del self.fields['phone']
|
||||
|
||||
items = self.event.items.filter(active=True).prefetch_related(
|
||||
'variations'
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if len(item.variations.all()) > 0:
|
||||
for variation in item.variations.all():
|
||||
if variation.active:
|
||||
choices.append(
|
||||
('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation)))
|
||||
)
|
||||
else:
|
||||
choices.append(('{}'.format(item.pk), str(item)))
|
||||
|
||||
self.fields['itemvar'].label = _("Product")
|
||||
self.fields['itemvar'].help_text = _("Only includes active products.")
|
||||
self.fields['itemvar'].required = True
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if self.instance.voucher is not None:
|
||||
raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.'))
|
||||
|
||||
itemvar = cleaned_data.get('itemvar')
|
||||
if itemvar:
|
||||
self.instance.item = self.event.items.get(pk=itemvar.split('-')[0])
|
||||
if '-' in itemvar:
|
||||
self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1])
|
||||
|
||||
if ((self.instance.item and not self.instance.item.active) or
|
||||
(self.instance.variation and not self.instance.variation.active)):
|
||||
self.add_error('itemvar', _('The selected product is not active.'))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = [
|
||||
'email',
|
||||
'name_parts',
|
||||
'phone',
|
||||
'subevent',
|
||||
]
|
||||
field_classes = {
|
||||
'subevent': SafeModelChoiceField,
|
||||
'email': forms.EmailField,
|
||||
'phone': PhoneNumberField,
|
||||
}
|
||||
widgets = {
|
||||
'phone': WrappedPhoneNumberPrefixWidget,
|
||||
}
|
||||
|
||||
@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
|
||||
'The order requires approval before it can continue to be processed.'),
|
||||
'pretix.event.order.approved': _('The order has been approved.'),
|
||||
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
|
||||
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
|
||||
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
|
||||
'to "{new_email}".'),
|
||||
'pretix.event.order.contact.confirmed': _(
|
||||
@@ -801,6 +802,8 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
||||
'pretix.giftcards.created': _('The gift card has been created.'),
|
||||
'pretix.giftcards.modified': _('The gift card has been changed.'),
|
||||
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
|
||||
'pretix.giftcards.transaction.payment': _('A payment has been performed.'),
|
||||
'pretix.giftcards.transaction.refund': _('A refund has been performed. '),
|
||||
'pretix.team.token.created': _('The token "{name}" has been created.'),
|
||||
'pretix.team.token.deleted': _('The token "{name}" has been revoked.'),
|
||||
'pretix.event.checkin.reset': _('The check-in and print log state has been reset.')
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
</ul>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if possible_cookie_problem %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please
|
||||
check if your browser is set to block cookies, or delete all existing cookies and retry.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
|
||||
@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
|
||||
If this code was not requested by you, please contact us immediately.
|
||||
|
||||
Best regards,
|
||||
Your pretix team
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to a team on pretix, a platform to perform event
|
||||
you have been invited to a team on {{ instance }}, a platform to perform event
|
||||
ticket sales.
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
||||
|
||||
this is to inform you that the account information of your pretix account has been
|
||||
this is to inform you that the account information of your {{ instance }} account has been
|
||||
changed. In particular, the following changes have been performed:
|
||||
|
||||
{{ messages }}
|
||||
@@ -12,5 +12,5 @@ You can review and change your account settings here:
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Your pretix team
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
|
||||
{% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %}
|
||||
|
||||
{% blocktrans asvar title_security_notice %}Customer account security notification{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="security_notice" title=title_security_notice items="mail_subject_customer_security_notice,mail_text_customer_security_notice" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "From" context "email" %}</dt>
|
||||
<dd>{{ mail.sender }}</dd>
|
||||
<dd>{{ sender }}</dd>
|
||||
<dt>{% trans "To" context "email" %}</dt>
|
||||
<dd>{{ mail.to|join:", " }}</dd>
|
||||
{% if mail.cc %}
|
||||
|
||||
@@ -144,14 +144,23 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
{% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
|
||||
{% blocktrans trimmed %}
|
||||
If you lose access to your devices, you can use one of your emergency tokens to log in.
|
||||
We recommend to store them in a safe place, e.g. printed out or in a password manager.
|
||||
Every token can be used at most once.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>{% trans "Unused tokens:" %}</p>
|
||||
<ul>
|
||||
{% for t in static_tokens %}
|
||||
<li><code>{{ t.token }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if static_tokens_device %}
|
||||
<p>
|
||||
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
|
||||
You generated your emergency tokens on {{ generation_date_time }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "You don't have any emergency tokens yet." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate new emergency tokens" %}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Edit entry" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Edit entry" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_field form.email layout="control" %}
|
||||
|
||||
{% if form.name_parts %}
|
||||
{% bootstrap_field form.name_parts layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.phone %}
|
||||
{% bootstrap_field form.phone layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -124,6 +124,7 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
@@ -267,13 +268,13 @@
|
||||
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
|
||||
<span class="fa fa-thumbs-down"></span>
|
||||
</button>
|
||||
{% if request.event.has_subevents %}
|
||||
<a href="{% url "control:event.orders.waitinglist.transfer" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
|
||||
class="btn btn-default btn-sm" title="{% trans "Transfer to other date" context "subevent" %}"
|
||||
data-toggle="tooltip">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
|
||||
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
|
||||
data-toggle="tooltip">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% else %}
|
||||
<button class="btn btn-default btn-sm disabled">
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Transfer entry" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Transfer entry" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans trimmed context "subevent" %}
|
||||
Please select the date to which the following waiting list entry should be
|
||||
transferred: <strong>{{ entry }}</strong>?
|
||||
{% endblocktrans %}</p>
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Transfer" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -480,8 +480,8 @@ urlpatterns = [
|
||||
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
|
||||
name='event.orders.waitinglist.delete'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
|
||||
name='event.orders.waitinglist.transfer'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
|
||||
name='event.orders.waitinglist.edit'),
|
||||
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
|
||||
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
|
||||
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||
|
||||
@@ -149,6 +149,8 @@ def login(request):
|
||||
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
|
||||
else:
|
||||
form = LoginForm(backend=backend, request=request)
|
||||
# Detect redirection loop (usually means cookie not accepted)
|
||||
ctx['possible_cookie_problem'] = request.path in request.headers.get("Referer", "")
|
||||
ctx['form'] = form
|
||||
ctx['can_register'] = settings.PRETIX_REGISTRATION
|
||||
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
|
||||
|
||||
@@ -870,11 +870,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
PlaceholderValidator.error_message)
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
PlaceholderValidator.error_message
|
||||
)
|
||||
except KeyError as e:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#
|
||||
import base64
|
||||
import logging
|
||||
from email.header import decode_header, make_header
|
||||
from email.utils import parseaddr
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -124,6 +126,12 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
if self.object.body_html:
|
||||
ctx['data_url'] = "data:text/html;charset=utf-8;base64," + base64.b64encode(self.object.body_html.encode()).decode()
|
||||
|
||||
from_name, from_email = parseaddr(self.object.sender)
|
||||
if from_name:
|
||||
from_name = make_header(decode_header(from_name))
|
||||
ctx['sender'] = "{} <{}>".format(from_name, from_email) if from_name else from_email
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -1226,7 +1226,11 @@ class OrderRefundView(OrderView):
|
||||
customer=order.customer,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
giftcard.log_action(
|
||||
action='pretix.giftcards.created',
|
||||
user=self.request.user,
|
||||
data={}
|
||||
)
|
||||
refunds.append(OrderRefund(
|
||||
order=order,
|
||||
payment=None,
|
||||
@@ -1637,9 +1641,17 @@ class OrderCheckVATID(OrderView):
|
||||
|
||||
try:
|
||||
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
|
||||
ia.vat_id_validated = True
|
||||
ia.vat_id = normalized_id
|
||||
ia.save()
|
||||
with transaction.atomic():
|
||||
ia.vat_id_validated = True
|
||||
ia.vat_id = normalized_id
|
||||
ia.save()
|
||||
self.order.log_action(
|
||||
'pretix.event.order.vatid.validated',
|
||||
data={
|
||||
'vat_id': normalized_id,
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
except VATIDFinalError as e:
|
||||
messages.error(self.request, e.message)
|
||||
except VATIDTemporaryError:
|
||||
|
||||
@@ -1039,9 +1039,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
def _send_invite(self, instance):
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
_('Account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'team': instance.team.name,
|
||||
@@ -1667,9 +1668,12 @@ class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermission
|
||||
active=False,
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.invited',
|
||||
data={'acceptor': form.cleaned_data['acceptor'].slug,
|
||||
'reusable_media': form.cleaned_data['reusable_media']},
|
||||
action='pretix.giftcards.acceptance.acceptor.invited',
|
||||
data={
|
||||
'acceptor': form.cleaned_data['acceptor'].slug,
|
||||
'issuer': self.request.organizer.slug,
|
||||
'reusable_media': form.cleaned_data['reusable_media']
|
||||
},
|
||||
user=self.request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected organizer has been invited.'))
|
||||
@@ -1705,8 +1709,11 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.removed',
|
||||
data={'acceptor': request.POST.get("delete_acceptor")},
|
||||
action='pretix.giftcards.acceptance.acceptor.removed',
|
||||
data={
|
||||
'acceptor': request.POST.get("delete_acceptor"),
|
||||
'issuer': self.request.organizer.slug
|
||||
},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
@@ -1716,8 +1723,11 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.removed',
|
||||
data={'issuer': request.POST.get("delete_acceptor")},
|
||||
action='pretix.giftcards.acceptance.issuer.removed',
|
||||
data={
|
||||
'issuer': request.POST.get("delete_acceptor"),
|
||||
'acceptor': self.request.organizer.slug
|
||||
},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
@@ -1727,8 +1737,11 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
|
||||
).update(active=True)
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.accepted',
|
||||
data={'issuer': request.POST.get("accept_issuer")},
|
||||
action='pretix.giftcards.acceptance.issuer.accepted',
|
||||
data={
|
||||
'issuer': request.POST.get("accept_issuer"),
|
||||
'acceptor': self.request.organizer.slug
|
||||
},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been accepted.'))
|
||||
@@ -1834,10 +1847,12 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
acceptor=request.organizer,
|
||||
)
|
||||
self.object.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
data={
|
||||
'value': value,
|
||||
'text': request.POST.get('text')
|
||||
'text': request.POST.get('text'),
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
@@ -1886,15 +1901,24 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
messages.success(self.request, _('The gift card has been created and can now be used.'))
|
||||
form.instance.issuer = self.request.organizer
|
||||
super().form_valid(form)
|
||||
form.instance.transactions.create(
|
||||
acceptor=self.request.organizer,
|
||||
value=form.cleaned_data['value']
|
||||
form.instance.log_action(
|
||||
action='pretix.giftcards.created',
|
||||
user=self.request.user,
|
||||
)
|
||||
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
if form.cleaned_data['value']:
|
||||
form.instance.log_action('pretix.giftcards.transaction.manual', user=self.request.user, data={
|
||||
'value': form.cleaned_data['value']
|
||||
})
|
||||
form.instance.transactions.create(
|
||||
acceptor=self.request.organizer,
|
||||
value=form.cleaned_data['value']
|
||||
)
|
||||
form.instance.log_action(
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'value': form.cleaned_data['value'],
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
return redirect(reverse(
|
||||
'control:organizer.giftcard',
|
||||
kwargs={
|
||||
@@ -1922,7 +1946,11 @@ class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The gift card has been changed.'))
|
||||
super().form_valid(form)
|
||||
form.instance.log_action('pretix.giftcards.modified', user=self.request.user, data=dict(form.cleaned_data))
|
||||
form.instance.log_action(
|
||||
action='pretix.giftcards.modified',
|
||||
user=self.request.user,
|
||||
data=dict(form.cleaned_data)
|
||||
)
|
||||
return redirect(reverse(
|
||||
'control:organizer.giftcard',
|
||||
kwargs={
|
||||
|
||||
@@ -49,12 +49,14 @@ from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
@@ -85,8 +87,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecentAuthenticationRequiredMixin:
|
||||
max_time = 3600
|
||||
max_time = 900
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
||||
if tdelta > self.max_time:
|
||||
@@ -289,16 +292,13 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
try:
|
||||
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
|
||||
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
||||
except StaticDevice.MultipleObjectsReturned:
|
||||
ctx['static_tokens'] = StaticDevice.objects.filter(
|
||||
ctx['static_tokens_device'] = StaticDevice.objects.filter(
|
||||
user=self.request.user, name='emergency'
|
||||
).first().token_set.all()
|
||||
).first()
|
||||
except StaticDevice.DoesNotExist:
|
||||
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
|
||||
for i in range(10):
|
||||
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
|
||||
ctx['static_tokens'] = d.token_set.all()
|
||||
ctx['static_tokens_device'] = None
|
||||
|
||||
ctx['devices'] = []
|
||||
for dt in REAL_DEVICE_TYPES:
|
||||
@@ -630,8 +630,14 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
|
||||
])
|
||||
self.request.user.update_session_token()
|
||||
update_session_auth_hash(self.request, self.request.user)
|
||||
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
|
||||
'place in case you lose access to your devices.'))
|
||||
messages.success(
|
||||
request,
|
||||
_('Your emergency codes have been newly generated. Remember to store them in a safe '
|
||||
'place in case you lose access to your devices. You will not be able to view them '
|
||||
'again here.\n\nYour emergency codes:\n{tokens}').format(
|
||||
tokens='- ' + '\n- '.join(t.token for t in d.token_set.all())
|
||||
)
|
||||
)
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.base.services.waitinglist import assign_automatically
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.waitinglist import WaitingListEntryTransferForm
|
||||
from pretix.control.forms.waitinglist import WaitingListEntryEditForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
|
||||
@@ -138,6 +138,17 @@ class WaitingListQuerySetMixin:
|
||||
elif force_filtered and '__ALL' not in self.request_data:
|
||||
qs = qs.none()
|
||||
|
||||
if self.request_data.get("search", "") != "":
|
||||
s = self.request_data.get("search", "")
|
||||
search_q = Q(email__icontains=s)
|
||||
|
||||
if self.request.event.settings.waiting_list_names_asked:
|
||||
search_q = search_q | Q(name_cached__icontains=s)
|
||||
if self.request.event.settings.waiting_list_phones_asked:
|
||||
search_q = search_q | Q(phone__icontains=s)
|
||||
|
||||
qs = qs.filter(search_q)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -238,7 +249,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = Item.objects.filter(event=self.request.event)
|
||||
ctx['filtered'] = ("status" in self.request.GET or "item" in self.request.GET)
|
||||
ctx['filtered'] = any(param in self.request.GET for param in ("status", "item", "search"))
|
||||
|
||||
itemvar_cache = {}
|
||||
quota_cache = {}
|
||||
@@ -390,25 +401,20 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
})
|
||||
|
||||
|
||||
class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
|
||||
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
|
||||
model = WaitingListEntry
|
||||
template_name = 'pretixcontrol/waitinglist/transfer.html'
|
||||
template_name = 'pretixcontrol/waitinglist/edit.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = WaitingListEntryTransferForm
|
||||
form_class = WaitingListEntryEditForm
|
||||
context_object_name = 'entry'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.request.event.has_subevents:
|
||||
raise Http404(_("This is not an event series."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_object(self, queryset=None) -> WaitingListEntry:
|
||||
return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The waitinglist entry has been transferred.'))
|
||||
if form.has_changed():
|
||||
messages.success(self.request, _('The waitinglist entry has been changed.'))
|
||||
self.object.log_action(
|
||||
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import logging
|
||||
from string import Formatter
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,6 +38,17 @@ class PlainHtmlAlternativeString:
|
||||
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
|
||||
|
||||
|
||||
class FormattedString(str):
|
||||
"""
|
||||
A str subclass that has been specifically marked as "already formatted" for email rendering
|
||||
purposes to avoid duplicate formatting.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
||||
|
||||
class SafeFormatter(Formatter):
|
||||
"""
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
@@ -77,8 +89,19 @@ class SafeFormatter(Formatter):
|
||||
# Ignore format_spec
|
||||
return super().format_field(self._prepare_value(value), '')
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
# Ignore any conversions
|
||||
if conversion is None:
|
||||
return value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
|
||||
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
|
||||
if isinstance(template, FormattedString):
|
||||
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
return FormattedString(
|
||||
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
)
|
||||
|
||||
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2735
-2270
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2661
-2277
File diff suppressed because it is too large
Load Diff
+2684
-2260
File diff suppressed because it is too large
Load Diff
+2581
-2267
File diff suppressed because it is too large
Load Diff
+2688
-2291
File diff suppressed because it is too large
Load Diff
+2661
-2268
File diff suppressed because it is too large
Load Diff
@@ -265,6 +265,7 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -539,6 +540,8 @@ WeChat-Zahlung
|
||||
Weiterleitungs-URIs
|
||||
Weiterleitungs-URL
|
||||
Weiterleitungs-URLs
|
||||
WERO
|
||||
WERO-App
|
||||
WhatsApp
|
||||
Widget
|
||||
Widget-Code
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -265,6 +265,7 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -539,6 +540,8 @@ WeChat-Zahlung
|
||||
Weiterleitungs-URIs
|
||||
Weiterleitungs-URL
|
||||
Weiterleitungs-URLs
|
||||
WERO
|
||||
WERO-App
|
||||
WhatsApp
|
||||
Widget
|
||||
Widget-Code
|
||||
|
||||
+2561
-2267
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 13:20+0000\n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+2708
-2288
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2687
-2292
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2563
-2267
File diff suppressed because it is too large
Load Diff
+2643
-2256
File diff suppressed because it is too large
Load Diff
+2684
-2258
File diff suppressed because it is too large
Load Diff
+2578
-2269
File diff suppressed because it is too large
Load Diff
+2685
-2285
File diff suppressed because it is too large
Load Diff
+2881
-2467
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2025-12-03 23:00+0000\n"
|
||||
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
|
||||
"PO-Revision-Date: 2026-03-02 21:00+0000\n"
|
||||
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
|
||||
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/gl/>\n"
|
||||
"Language: gl\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.14.3\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -162,12 +162,12 @@ msgstr "Pedidos pagados"
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Attendees (ordered)"
|
||||
msgstr ""
|
||||
msgstr "Asistentes (ordenados)"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Attendees (paid)"
|
||||
msgstr ""
|
||||
msgstr "Asistentes (de pago)"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
|
||||
msgid "Total revenue"
|
||||
@@ -732,8 +732,8 @@ msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
|
||||
"completar o teu pedido mentres estean dispoñibles."
|
||||
"Os artigos do teu carro xa non están reservados para ti. Podes completar o "
|
||||
"teu pedido sempre que estean dispoñibles."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
||||
msgid "Cart expired"
|
||||
|
||||
+2853
-2455
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2662
-2254
File diff suppressed because it is too large
Load Diff
+2654
-2286
File diff suppressed because it is too large
Load Diff
+2716
-2259
File diff suppressed because it is too large
Load Diff
+2634
-2262
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 09:40+0000\n"
|
||||
"Last-Translator: \"Luca Martinelli [Sannita]\" <sannita@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-02-10 16:49+0000\n"
|
||||
"Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.11.1\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -1134,6 +1134,9 @@ msgid ""
|
||||
"add yourself to the waiting list. We will then notify if seats are available "
|
||||
"again."
|
||||
msgstr ""
|
||||
"Alcune o tutte le categorie di biglietti sono attualmente esaurite. Se lo "
|
||||
"desideri, puoi aggiungerti alla lista d'attesa. Ti informeremo se i posti "
|
||||
"saranno nuovamente disponibili."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgctxt "widget"
|
||||
|
||||
+3712
-3335
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2026-01-28 18:00+0000\n"
|
||||
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
|
||||
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
"X-Generator: Weblate 5.16\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -27,7 +27,7 @@ msgstr "支払い済み"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||
msgid "Comment:"
|
||||
msgstr "注釈:"
|
||||
msgstr "コメント:"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||
msgid "PayPal"
|
||||
@@ -157,7 +157,7 @@ msgstr "受注件数"
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63
|
||||
msgid "Paid orders"
|
||||
msgstr "支払い済みの注文"
|
||||
msgstr "支払い済み件数"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
@@ -167,7 +167,7 @@ msgstr "参加者 (注文済み)"
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Attendees (paid)"
|
||||
msgstr "参加者 (有料)"
|
||||
msgstr "参加者 (支払い済み)"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
|
||||
msgid "Total revenue"
|
||||
@@ -215,7 +215,7 @@ msgstr "このチケットは特別な対応が必要です"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
|
||||
msgid "Switch direction"
|
||||
msgstr "方向転換"
|
||||
msgstr "向きを切り替え"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
|
||||
msgid "Entry"
|
||||
@@ -223,7 +223,7 @@ msgstr "入場"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
|
||||
msgid "Exit"
|
||||
msgstr "退出"
|
||||
msgstr "退場"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
|
||||
msgid "Scan a ticket or search and press return…"
|
||||
@@ -256,7 +256,7 @@ msgstr "承認保留中"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||
msgid "Redeemed"
|
||||
msgstr "使用済"
|
||||
msgstr "引き換え済み"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||
msgid "Cancel"
|
||||
@@ -297,7 +297,7 @@ msgstr "不明なチケット"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||
msgid "Ticket type not allowed here"
|
||||
msgstr "この種類のチケットは使用できません"
|
||||
msgstr "この種類のチケットはここでは使用できません"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||
msgid "Entry not allowed"
|
||||
@@ -321,7 +321,7 @@ msgstr "注文がキャンセルされました"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||
msgid "Ticket code is ambiguous on list"
|
||||
msgstr "リストのチケットコードは曖昧です"
|
||||
msgstr "リスト上でチケットコードが一意に特定できません"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||
msgid "Order not approved"
|
||||
@@ -358,7 +358,7 @@ msgstr "閉じる"
|
||||
#: pretix/static/pretixbase/js/addressform.js:101
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
msgid "required"
|
||||
msgstr "必要"
|
||||
msgstr "必須"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:13
|
||||
msgid ""
|
||||
@@ -370,8 +370,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:17
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
"サーバへ送信されたリクエスト順にお応えしています。今しばらくお待ちください。"
|
||||
msgstr "お客様のリクエストはサーバーで受け付けられました。まもなく処理されます。"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:21
|
||||
msgid ""
|
||||
@@ -394,8 +393,7 @@ msgstr "{code} のエラーが発生しました。"
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"現在サーバへの接続ができませんが、接続試行中です。エラーコード: {code}"
|
||||
msgstr "現在サーバへの接続ができませんが、接続試行中です。最新のエラーコード: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:162
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
@@ -419,9 +417,8 @@ msgid ""
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
"リクエストがサーバへ送信されました。1分以上経っても応答がない場合は、イン"
|
||||
"ターネット接続を確認してください。確認完了後、ウェブページを再度読込み、再試"
|
||||
"行してください。"
|
||||
"現在リクエストをサーバへ送信中です。1分以上経っても応答がない場合は、"
|
||||
"インターネット接続を確認し、このページを再読み込みして再試行してください。"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:276
|
||||
msgid "If this takes longer than a few minutes, please contact us."
|
||||
@@ -429,7 +426,7 @@ msgstr "数分以上かかる場合は、お問い合わせください。"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:331
|
||||
msgid "Close message"
|
||||
msgstr "閉じる"
|
||||
msgstr "メッセージを閉じる"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/clipboard.js:23
|
||||
msgid "Copied!"
|
||||
@@ -443,7 +440,7 @@ msgstr "Ctrl-Cを押してコピー!"
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:18
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:24
|
||||
msgid "is one of"
|
||||
msgstr "の一つです"
|
||||
msgstr "次のいずれか"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:30
|
||||
msgid "is before"
|
||||
@@ -523,7 +520,7 @@ msgstr "以下のすべての条件(AND)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:183
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "以下の条件のうち、最低1つ(または)"
|
||||
msgstr "以下の条件のうち、最低1つ(OR)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:184
|
||||
msgid "Event start"
|
||||
@@ -564,16 +561,16 @@ msgstr "複製"
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
|
||||
msgctxt "entry_status"
|
||||
msgid "present"
|
||||
msgstr "出席"
|
||||
msgstr "入場中"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
|
||||
msgctxt "entry_status"
|
||||
msgid "absent"
|
||||
msgstr "欠席"
|
||||
msgstr "未入場"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:289
|
||||
msgid "Error: Product not found!"
|
||||
msgstr "エラー:製品が見つかりません!"
|
||||
msgstr "エラー:商品が見つかりません!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:296
|
||||
msgid "Error: Variation not found!"
|
||||
@@ -585,7 +582,7 @@ msgstr "チェックイン用QRコード"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:549
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "以下の理由によりPDFファイルの読み込みに失敗しました:"
|
||||
msgstr "以下の理由によりPDF背景ファイルの読み込みに失敗しました:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:904
|
||||
msgid "Group of objects"
|
||||
@@ -593,7 +590,7 @@ msgstr "オブジェクトグループ"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:909
|
||||
msgid "Text object (deprecated)"
|
||||
msgstr "テキストオブジェクト (廃止済)"
|
||||
msgstr "テキストオブジェクト(非推奨)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:911
|
||||
msgid "Text box"
|
||||
@@ -609,7 +606,7 @@ msgstr "画像エリア"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:917
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Pretixのイベントチケット売り場"
|
||||
msgstr "Powered by pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:919
|
||||
msgid "Object"
|
||||
@@ -647,24 +644,24 @@ msgstr "不明なエラー。"
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:309
|
||||
msgid "Your color has great contrast and will provide excellent accessibility."
|
||||
msgstr ""
|
||||
"あなたの色は素晴らしいコントラストを持ち、優れたアクセシビリティを提供しま"
|
||||
"す。"
|
||||
"選択した色は素晴らしいコントラストを持ち、優れたアクセシビリティを提供します"
|
||||
"。"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:313
|
||||
msgid ""
|
||||
"Your color has decent contrast and is sufficient for minimum accessibility "
|
||||
"requirements."
|
||||
msgstr ""
|
||||
"あなたの色は適切なコントラストを持ち、最小限のアクセシビリティ要件に十分で"
|
||||
"す。"
|
||||
"選択した色は適切なコントラストを持ち、最小限のアクセシビリティ要件に十分です"
|
||||
"。"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:317
|
||||
msgid ""
|
||||
"Your color has insufficient contrast to white. Accessibility of your site "
|
||||
"will be impacted."
|
||||
msgstr ""
|
||||
"あなたの色は白に対して十分なコントラストがありません。サイトのアクセシビリ"
|
||||
"ティに影響します。"
|
||||
"選択した色は白に対して十分なコントラストがありません。サイトの"
|
||||
"アクセシビリティに影響します。"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:443
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:463
|
||||
@@ -677,7 +674,7 @@ msgstr "全て"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "None"
|
||||
msgstr "ない"
|
||||
msgstr "なし"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:466
|
||||
msgid "Selected only"
|
||||
@@ -693,7 +690,7 @@ msgstr "無効なページ番号。"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1000
|
||||
msgid "Use a different name internally"
|
||||
msgstr "内部で別の名前を使用してください"
|
||||
msgstr "内部で別の名前を使用する"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1040
|
||||
msgid "Click to close"
|
||||
@@ -722,15 +719,15 @@ msgstr "カウント"
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:112
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
msgstr[0] "({num} 他の日程)"
|
||||
msgstr[0] "(他に{num}件の日程)"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:47
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"カートに入っている商品は現在売り切れです。在庫があれば、このまま注文を完了す"
|
||||
"ることができます。"
|
||||
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
|
||||
"できます。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
||||
msgid "Cart expired"
|
||||
@@ -744,27 +741,27 @@ msgstr "カートの有効期限が近づいています。"
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:62
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "カート内の商品の予約は {num} 分以内に完了します。"
|
||||
msgstr[0] "カート内の商品はあと {num} 分間確保されています。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:83
|
||||
msgid "Your cart has expired."
|
||||
msgstr "カートの保存期限が切れています。"
|
||||
msgstr "カートの有効期限が切れています。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:86
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they're available."
|
||||
msgstr ""
|
||||
"カート内の商品の予約期限が切れました。在庫があれば、このまま注文を完了するこ"
|
||||
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
|
||||
"とができます。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:87
|
||||
msgid "Do you want to renew the reservation period?"
|
||||
msgstr "予約の期間を更新しますか?"
|
||||
msgstr "確保期間を更新しますか?"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:90
|
||||
msgid "Renew reservation"
|
||||
msgstr "予約を更新"
|
||||
msgstr "確保を更新"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:194
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
@@ -780,7 +777,7 @@ msgstr "主催者が留保する料金を入力してください。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:577
|
||||
msgid "Your local time:"
|
||||
msgstr "現地時間:"
|
||||
msgstr "お使いの地域の時刻:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/walletdetection.js:39
|
||||
msgid "Google Pay"
|
||||
@@ -863,7 +860,7 @@ msgstr "登録"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid "Reserved"
|
||||
msgstr "予約完了"
|
||||
msgstr "予約済み"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
msgctxt "widget"
|
||||
@@ -879,7 +876,7 @@ msgstr "%(currency)s %(price)sから"
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "Image of %s"
|
||||
msgstr "%sのイメージ"
|
||||
msgstr "%sの画像"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
msgctxt "widget"
|
||||
@@ -889,7 +886,7 @@ msgstr "%(rate)s% %(taxname)s込"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr "%(rate)s% %(taxname)s抜"
|
||||
msgstr "別途%(rate)s% %(taxname)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
@@ -899,13 +896,13 @@ msgstr "税込"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "plus taxes"
|
||||
msgstr "税抜"
|
||||
msgstr "税別"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgstr "現在%s使用可能"
|
||||
msgstr "現在の残数: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
@@ -921,7 +918,7 @@ msgstr "提供開始前"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr "今後の提供不可"
|
||||
msgstr "提供終了"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
@@ -937,7 +934,7 @@ msgstr "最小注文数量:%s"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "チケットショップ閉店"
|
||||
msgstr "チケットショップを閉じる"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgctxt "widget"
|
||||
@@ -950,8 +947,8 @@ msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
"現在チケットショップが混み合っています。新しいタブでチケットショップを開き続"
|
||||
"行してください。"
|
||||
"現在チケットショップが混み合っています。新しいタブでチケットショップを開いて"
|
||||
"続行してください。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
@@ -975,7 +972,7 @@ msgid ""
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
"現在チケットショップが混雑しているため、お客様のカートを作ることができません"
|
||||
"でした。新しいタブを開き「次へ」をクリックしてください。"
|
||||
"でした。\"続ける\"をクリックして、新しいタブで再試行してください。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
@@ -988,8 +985,8 @@ msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
"お客様のカートはイベントの申し込みに有効です。商品を選択し、カートへ追加して"
|
||||
"ください。"
|
||||
"このイベントのカートに商品が入っています。商品を追加すると、既存のカートに追"
|
||||
"加されます。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
@@ -1079,7 +1076,7 @@ msgstr "前週"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr "座席一覧を開く"
|
||||
msgstr "座席選択を開く"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgctxt "widget"
|
||||
|
||||
+2643
-2260
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2025-11-18 17:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2026-02-01 21:00+0000\n"
|
||||
"Last-Translator: z3rrry <z3rrry@gmail.com>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"ko/>\n"
|
||||
"Language: ko\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.14.3\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -175,12 +175,12 @@ msgstr "유료 주문"
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Attendees (ordered)"
|
||||
msgstr ""
|
||||
msgstr "참가자 (정렬된)"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
msgid "Attendees (paid)"
|
||||
msgstr ""
|
||||
msgstr "참가자 (결제된)"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
|
||||
msgid "Total revenue"
|
||||
|
||||
+2648
-2276
File diff suppressed because it is too large
Load Diff
+2563
-2267
File diff suppressed because it is too large
Load Diff
+2648
-2260
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3373
-2990
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user