Compare commits

...

23 Commits

Author SHA1 Message Date
Richard Schreiber
dd5fd41211 Fix: overwrite widget_data for existing carts (Z#23181715) 2025-02-25 10:14:23 +01:00
dependabot[bot]
551e219d9a Update sentry-sdk requirement from ==2.20.* to ==2.22.* (#4855)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.20.0...2.22.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 18:02:49 +01:00
Kian Cross
352d4e29f1 Allow ticket QR code colour to be configured (#4726)
* Allow ticket QR code colour to be configured

This commit introduces a feature enabling users to customise the QR
code colour in the ticket editor.

* Remove redundant argument from `Dict.get` call

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

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2025-02-24 17:28:59 +01:00
Mira
4872082780 Prioritize widget-provided cookie consent information over localStorage (Z#23181715) (#4862)
* Prioritize widget-provided cookie consent information over localStorage

* Hide cookie-consent-reopen link if external consent info from widget present
2025-02-24 17:15:03 +01:00
Mira
fb6117a307 Fix voucher application on "free price" items (Z#23183254) (#4863)
* Do not apply vouchers on "free price" items where more than minimum price is selected

* Do apply vouchers on "free price" items if exactly the minimum price is selected

* Update cart.py

* Add test cases, fix bug in adjacent test

* Fix code style

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2025-02-24 17:14:49 +01:00
dependabot[bot]
1af7636aa3 Bump django-filter from 24.3 to 25.1
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 24.3 to 25.1.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/24.3...25.1)

---
updated-dependencies:
- dependency-name: django-filter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:22:31 +01:00
anonymous
78b608aee3 Translations: Update Slovak
Currently translated at 90.1% (5270 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
96f00a171b Translations: Update Chinese (Traditional Han script)
Currently translated at 96.9% (5665 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
5ea2abd682 Translations: Update Ukrainian
Currently translated at 60.6% (3544 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
faec3fd2ed Translations: Update Galician
Currently translated at 9.6% (567 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
dbacd9f96c Translations: Update Latvian
Currently translated at 36.1% (2113 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
dbac2c279e Translations: Update Russian
Currently translated at 18.5% (1087 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
anonymous
0af60eddb1 Translations: Update Arabic
Currently translated at 63.5% (3714 of 5846 strings)

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

powered by weblate
2025-02-24 16:16:38 +01:00
Raphael Michel
d721d9577c Improve efficiency of bulk operations (#4832)
* Improve efficiency of bulk operations

* Update src/pretix/base/models/log.py

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

* Review notes

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-02-21 16:11:47 +01:00
Raphael Michel
67db230f72 OIDC: Implement PKCE in OP and RP 2025-02-21 13:23:17 +01:00
Raphael Michel
78fbda2311 Add Croatian as a language 2025-02-21 11:15:06 +01:00
Robert Rigo
6e4b2e500c Translations: Update Croatian
Currently translated at 84.3% (4933 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
Robert Rigo
e124e4d1fc Translations: Update Croatian
Currently translated at 50.5% (2958 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
Petr Čermák
73e9b6c485 Translations: Update Czech
Currently translated at 86.8% (204 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/cs/

powered by weblate
2025-02-21 11:09:00 +01:00
Petr Čermák
6e010d367c Translations: Update Czech
Currently translated at 74.0% (4327 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
Renne Rocha
ea5ee3d5d6 Translations: Update Portuguese (Brazil)
Currently translated at 18.8% (1104 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
Robert Rigo
8ec6138602 Translations: Update Croatian
Currently translated at 18.4% (1078 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
deborahfoell
2f4aeee42f Translations: Update Greek
Currently translated at 47.5% (2782 of 5846 strings)

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

powered by weblate
2025-02-21 11:09:00 +01:00
37 changed files with 6017 additions and 4975 deletions

View File

@@ -39,7 +39,7 @@ dependencies = [
"django-bootstrap3==24.3", "django-bootstrap3==24.3",
"django-compressor==4.5.1", "django-compressor==4.5.1",
"django-countries==7.6.*", "django-countries==7.6.*",
"django-filter==24.3", "django-filter==25.1",
"django-formset-js-improved==0.5.0.3", "django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1", "django-formtools==2.5.1",
"django-hierarkey==1.2.*", "django-hierarkey==1.2.*",
@@ -91,7 +91,7 @@ dependencies = [
"redis==5.2.*", "redis==5.2.*",
"reportlab==4.3.*", "reportlab==4.3.*",
"requests==2.31.*", "requests==2.31.*",
"sentry-sdk==2.20.*", "sentry-sdk==2.22.*",
"sepaxml==2.6.*", "sepaxml==2.6.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",

View File

@@ -93,6 +93,7 @@ ALL_LANGUAGES = [
('zh-hans', _('Chinese (simplified)')), ('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')), ('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')), ('cs', _('Czech')),
('hr', _('Croatian')),
('da', _('Danish')), ('da', _('Danish')),
('nl', _('Dutch')), ('nl', _('Dutch')),
('nl-informal', _('Dutch (informal)')), ('nl-informal', _('Dutch (informal)')),

View File

@@ -148,7 +148,7 @@ def oidc_validate_and_complete_config(config):
return config return config
def oidc_authorize_url(provider, state, redirect_uri): def oidc_authorize_url(provider, state, redirect_uri, pkce_code_verifier):
endpoint = provider.configuration['provider_config']['authorization_endpoint'] endpoint = provider.configuration['provider_config']['authorization_endpoint']
params = { params = {
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
@@ -163,10 +163,14 @@ def oidc_authorize_url(provider, state, redirect_uri):
if "query_parameters" in provider.configuration and provider.configuration["query_parameters"]: if "query_parameters" in provider.configuration and provider.configuration["query_parameters"]:
params.update(parse_qsl(provider.configuration["query_parameters"])) params.update(parse_qsl(provider.configuration["query_parameters"]))
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
params["code_challenge"] = base64.urlsafe_b64encode(hashlib.sha256(pkce_code_verifier.encode()).digest()).decode().rstrip("=")
params["code_challenge_method"] = "S256"
return endpoint + '?' + urlencode(params) return endpoint + '?' + urlencode(params)
def oidc_validate_authorization(provider, code, redirect_uri): def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier):
endpoint = provider.configuration['provider_config']['token_endpoint'] endpoint = provider.configuration['provider_config']['token_endpoint']
# Wall of shame and RFC ignorant IDPs # Wall of shame and RFC ignorant IDPs
@@ -188,6 +192,9 @@ def oidc_validate_authorization(provider, code, redirect_uri):
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
} }
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
params["code_verifier"] = pkce_code_verifier
if token_endpoint_auth_method == 'client_secret_post': if token_endpoint_auth_method == 'client_secret_post':
params['client_id'] = provider.configuration['client_id'] params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret'] params['client_secret'] = provider.configuration['client_secret']

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2.17 on 2025-02-07 16:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0276_item_hidden_if_item_available_mode"),
]
operations = [
migrations.AddField(
model_name="customerssoclient",
name="require_pkce",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="customerssogrant",
name="code_challenge",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="customerssogrant",
name="code_challenge_method",
field=models.CharField(max_length=255, null=True),
),
]

View File

@@ -416,6 +416,10 @@ class CustomerSSOClient(LoggedModel):
authorization_grant_type = models.CharField( authorization_grant_type = models.CharField(
max_length=32, choices=GRANT_TYPES, verbose_name=_("Grant type"), default=GRANT_AUTHORIZATION_CODE, max_length=32, choices=GRANT_TYPES, verbose_name=_("Grant type"), default=GRANT_AUTHORIZATION_CODE,
) )
require_pkce = models.BooleanField(
verbose_name=_("Require PKCE extension"),
default=False,
)
redirect_uris = models.TextField( redirect_uris = models.TextField(
blank=False, blank=False,
verbose_name=_("Redirection URIs"), verbose_name=_("Redirection URIs"),
@@ -481,6 +485,8 @@ class CustomerSSOGrant(models.Model):
expires = models.DateTimeField() expires = models.DateTimeField()
redirect_uri = models.TextField() redirect_uri = models.TextField()
scope = models.TextField(blank=True) scope = models.TextField(blank=True)
code_challenge = models.TextField(blank=True, null=True)
code_challenge_method = models.CharField(max_length=255, blank=True, null=True)
class CustomerSSOAccessToken(models.Model): class CustomerSSOAccessToken(models.Model):

View File

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

View File

@@ -822,6 +822,10 @@ class Renderer:
kwargs = {} kwargs = {}
if o.get('nowhitespace', False): if o.get('nowhitespace', False):
kwargs['barBorder'] = 0 kwargs['barBorder'] = 0
if o.get('color'):
kwargs['barFillColor'] = Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255)
qrw = QrCodeWidget(content, barLevel=level, barHeight=reqs, barWidth=reqs, **kwargs) qrw = QrCodeWidget(content, barLevel=level, barHeight=reqs, barWidth=reqs, **kwargs)
d = Drawing(reqs, reqs) d = Drawing(reqs, reqs)
d.add(qrw) d.add(qrw)

View File

@@ -624,6 +624,9 @@ class CartManager:
if p.is_bundled: if p.is_bundled:
continue continue
if p.custom_price_input and p.custom_price_input != p.listed_price:
continue
if p.listed_price is None: if p.listed_price is None:
if p.addon_to_id and is_included_for_free(p.item, p.addon_to): if p.addon_to_id and is_included_for_free(p.item, p.addon_to):
listed_price = Decimal('0.00') listed_price = Decimal('0.00')
@@ -1346,8 +1349,10 @@ class CartManager:
op.position.price_after_voucher = op.price_after_voucher op.position.price_after_voucher = op.price_after_voucher
op.position.voucher = op.voucher op.position.voucher = op.voucher
if op.position.custom_price_input and op.position.custom_price_input == op.position.listed_price:
op.position.custom_price_input = op.price_after_voucher
# op.position.price will be set in recompute_final_prices_and_taxes # op.position.price will be set in recompute_final_prices_and_taxes
op.position.save(update_fields=['price_after_voucher', 'voucher']) op.position.save(update_fields=['price_after_voucher', 'voucher', 'custom_price_input'])
vouchers_ok[op.voucher] -= 1 vouchers_ok[op.voucher] -= 1
if op.voucher.all_bundles_included or op.voucher.all_addons_included: if op.voucher.all_bundles_included or op.voucher.all_addons_included:

View File

@@ -1113,7 +1113,7 @@ class SSOClientForm(I18nModelForm):
class Meta: class Meta:
model = CustomerSSOClient model = CustomerSSOClient
fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris', fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
'allowed_scopes'] 'allowed_scopes', 'require_pkce']
widgets = { widgets = {
'authorization_grant_type': forms.RadioSelect, 'authorization_grant_type': forms.RadioSelect,
'client_type': forms.RadioSelect, 'client_type': forms.RadioSelect,

View File

@@ -322,6 +322,17 @@
id="toolbox-squaresize"> id="toolbox-squaresize">
</div> </div>
</div> </div>
<div class="row control-group squaresize">
<div class="col-sm-12">
<label for="toolbox-col">{% trans "QR color" %}</label><br>
<div class="input-group">
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-qrcolor">
<span class="input-group-addon contrast-icon">
</span>
</div>
</div>
</div>
<div class="row control-group squaresize"> <div class="row control-group squaresize">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="checkbox"> <div class="checkbox">

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-09-09 06:00+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Ahmad AlHarthi <to.ahmad@pm.me>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/ar/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/ar/"
">\n" ">\n"
"Language: ar\n" "Language: ar\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" "&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 5.7\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -21685,6 +21685,7 @@ msgid "Items"
msgstr "العناصر" msgstr "العناصر"
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:31 #: pretix/control/templates/pretixcontrol/items/quota_edit.html:31
#, fuzzy
msgid "" msgid ""
"Please select the products or product variations this quota should be " "Please select the products or product variations this quota should be "
"applied to. If you apply two quotas to the same product, it will only be " "applied to. If you apply two quotas to the same product, it will only be "
@@ -29509,7 +29510,7 @@ msgid "Export SEPA xml"
msgstr "تصدير SEPA xml" msgstr "تصدير SEPA xml"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:13 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:13
#, python-format #, fuzzy, python-format
msgid "" msgid ""
"You are trying to download a refund export from %(date)s with one order and " "You are trying to download a refund export from %(date)s with one order and "
"a total of %(sum)s." "a total of %(sum)s."
@@ -29517,20 +29518,18 @@ msgid_plural ""
"You are trying to download a refund export from %(date)s with %(cnt)s order " "You are trying to download a refund export from %(date)s with %(cnt)s order "
"and a total of %(sum)s." "and a total of %(sum)s."
msgstr[0] "" msgstr[0] ""
"تحاول تنزيل اصدار استرداد من%(date)s لصفر طلب %(cnt)s وبمبلغ اجمالي %(sum)s." "تحاول تنزيل اصدار استرداد من%(date)s لصفر طلب %(cnt)s وبمبلغ اجمالي %(sum)"
"s.تحاول تنزيل اصدار استرداد من%(date)s لطلب واحدs%(cnt)sوبمبلغ اجمالي %(sum)"
"s.تحاول تنزيل اصدار استرداد من%(date)s لطلبين s%(cnt)sوبمبلغ اجمالي %(sum)"
"s.تحاول تنزيل اصدار استرداد من%(date)s لعدة طلبات s%(cnt)sوبمبلغ اجمالي "
"%(sum)s.تحاول تنزيل اصدار استرداد من%(date)s لطلبات كثيرة s%(cnt)sوبمبلغ "
"اجمالي %(sum)s.تحاول تنزيل اصدار استرداد من%(date)s لطلبات غير محددة %(cnt)"
"sوبمبلغ اجمالي %(sum)s."
msgstr[1] "" msgstr[1] ""
"تحاول تنزيل اصدار استرداد من%(date)s لطلب واحدs%(cnt)sوبمبلغ اجمالي %(sum)s."
msgstr[2] "" msgstr[2] ""
"تحاول تنزيل اصدار استرداد من%(date)s لطلبين s%(cnt)sوبمبلغ اجمالي %(sum)s."
msgstr[3] "" msgstr[3] ""
"تحاول تنزيل اصدار استرداد من%(date)s لعدة طلبات s%(cnt)sوبمبلغ اجمالي "
"%(sum)s."
msgstr[4] "" msgstr[4] ""
"تحاول تنزيل اصدار استرداد من%(date)s لطلبات كثيرة s%(cnt)sوبمبلغ اجمالي "
"%(sum)s."
msgstr[5] "" msgstr[5] ""
"تحاول تنزيل اصدار استرداد من%(date)s لطلبات غير محددة %(cnt)sوبمبلغ اجمالي "
"%(sum)s."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:23 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:23
msgid "" msgid ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2023-09-15 06:00+0000\n" "PO-Revision-Date: 2025-02-19 17:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n" "Last-Translator: Petr Čermák <pcermak@live.com>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
"cs/>\n" "cs/>\n"
"Language: cs\n" "Language: cs\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 5.0.1\n" "X-Generator: Weblate 5.10\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -314,7 +314,7 @@ msgstr "Kód vstupenky je v seznamu nejednoznačný"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "" msgstr "Objednávka nebyla potvrzena"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"

View File

@@ -8,10 +8,10 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-12-22 00:00+0000\n" "PO-Revision-Date: 2025-02-14 21:00+0000\n"
"Last-Translator: Dimitris Tsimpidis <tsimpidisd@gmail.com>\n" "Last-Translator: deborahfoell <deborah.foell@om.org>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/" "Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/>"
">\n" "\n"
"Language: el\n" "Language: el\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -1443,7 +1443,7 @@ msgstr "Κωδικός παραγγελίας"
#: pretix/control/views/waitinglist.py:307 #: pretix/control/views/waitinglist.py:307
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:134 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:134
msgid "Email address" msgid "Email address"
msgstr "Διεύθυνση ηλεκτρονικού ταχυδρομείου" msgstr "ηλεκτρονική διεύθυνση"
#: pretix/base/exporters/invoices.py:203 pretix/base/exporters/invoices.py:330 #: pretix/base/exporters/invoices.py:203 pretix/base/exporters/invoices.py:330
msgid "Invoice type" msgid "Invoice type"
@@ -34211,7 +34211,7 @@ msgstr ""
#: pretix/presale/forms/checkout.py:70 #: pretix/presale/forms/checkout.py:70
msgid "Email address (repeated)" msgid "Email address (repeated)"
msgstr "Διεύθυνση ηλεκτρονικού ταχυδρομείου (επαναλαμβανόμενη)" msgstr "επαλήθευση ηλεκτρονικής διεύθυνσης"
#: pretix/presale/forms/checkout.py:71 #: pretix/presale/forms/checkout.py:71
msgid "" msgid ""

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-07-30 08:36+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/"
"gl/>\n" "gl/>\n"
"Language: gl\n" "Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.6.2\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
#, fuzzy #, fuzzy
@@ -11272,7 +11272,7 @@ msgid "Your orders for {event}"
msgstr "Os teus pedidos de {event}" msgstr "Os teus pedidos de {event}"
#: pretix/base/settings.py:2192 #: pretix/base/settings.py:2192
#, python-brace-format #, fuzzy, python-brace-format
msgid "" msgid ""
"Hello,\n" "Hello,\n"
"\n" "\n"
@@ -35837,7 +35837,7 @@ msgid "Print"
msgstr "Pie de imprenta" msgstr "Pie de imprenta"
#: pretix/presale/templates/pretixpresale/event/order.html:152 #: pretix/presale/templates/pretixpresale/event/order.html:152
#, python-format #, fuzzy, python-format
msgid "" msgid ""
"We've issued your refund of %(amount)s as a gift card. On your next purchase " "We've issued your refund of %(amount)s as a gift card. On your next purchase "
"with us, you can use the following gift card code during payment:" "with us, you can use the following gift card code during payment:"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-09-16 13:00+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Svyatoslav <slava@digitalarthouse.eu>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix/"
"lv/>\n" "lv/>\n"
"Language: lv\n" "Language: lv\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= " "Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= "
"19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" "19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n"
"X-Generator: Weblate 5.7\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -31377,12 +31377,14 @@ msgid "Usage:"
msgstr "Lietošana:" msgstr "Lietošana:"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:138 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:138
#, python-format #, fuzzy, python-format
msgid "This ticket has been used once." msgid "This ticket has been used once."
msgid_plural "This ticket has been used %(count)s times." msgid_plural "This ticket has been used %(count)s times."
msgstr[0] "Šī biļete jau ir izmantota." msgstr[0] ""
msgstr[1] "Šī biļete jau %(count)s reiz ir izmantota." "Šī biļete jau ir izmantota.Šī biļete jau %(count)s reiz ir izmantota.Šī "
msgstr[2] "Šī biļete jau ir izmantota %(count)s reizes." "biļete jau ir izmantota %(count)s reizes."
msgstr[1] ""
msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:170 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:170
msgid "No attendee name provided" msgid "No attendee name provided"
@@ -31474,12 +31476,12 @@ msgid "Current value:"
msgstr "Šī brīža vērtība" msgstr "Šī brīža vērtība"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463
#, python-format #, fuzzy, python-format
msgid "One product" msgid "One product"
msgid_plural "%(num)s products" msgid_plural "%(num)s products"
msgstr[0] "0 produktu" msgstr[0] "0 produktu1 produkts%(num)s produkti"
msgstr[1] "1 produkts" msgstr[1] ""
msgstr[2] "%(num)s produkti" msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477
#, fuzzy, python-format #, fuzzy, python-format

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2025-01-31 01:00+0000\n" "PO-Revision-Date: 2025-02-18 11:00+0000\n"
"Last-Translator: Lorhan Sohaky <lorhansohaky@gmail.com>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n" "pretix/pretix/pt_BR/>\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.9.2\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -3321,11 +3321,10 @@ msgid "Single price: {net_price} net / {gross_price} gross"
msgstr "Preço único: {net_price} líquido / {gross_price} bruto" msgstr "Preço único: {net_price} líquido / {gross_price} bruto"
#: pretix/base/invoice.py:668 #: pretix/base/invoice.py:668
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "Original price"
msgctxt "invoice" msgctxt "invoice"
msgid "Single price: {price}" msgid "Single price: {price}"
msgstr "Preço original" msgstr "Preço simples: {price}"
#: pretix/base/invoice.py:686 pretix/base/invoice.py:692 #: pretix/base/invoice.py:686 pretix/base/invoice.py:692
msgctxt "invoice" msgctxt "invoice"
@@ -3333,11 +3332,9 @@ msgid "Invoice total"
msgstr "Total da fatura" msgstr "Total da fatura"
#: pretix/base/invoice.py:702 #: pretix/base/invoice.py:702
#, fuzzy
#| msgid "Paid orders"
msgctxt "invoice" msgctxt "invoice"
msgid "Received payments" msgid "Received payments"
msgstr "Ordens pagas" msgstr "Pagamentos recebidos"
#: pretix/base/invoice.py:707 #: pretix/base/invoice.py:707
msgctxt "invoice" msgctxt "invoice"
@@ -3345,18 +3342,14 @@ msgid "Outstanding payments"
msgstr "Pagamentos pendentes" msgstr "Pagamentos pendentes"
#: pretix/base/invoice.py:724 #: pretix/base/invoice.py:724
#, fuzzy
#| msgid "Gift card"
msgctxt "invoice" msgctxt "invoice"
msgid "Paid by gift card" msgid "Paid by gift card"
msgstr "Cartão Presente" msgstr "Pago com cartão-presente"
#: pretix/base/invoice.py:729 #: pretix/base/invoice.py:729
#, fuzzy
#| msgid "Payment date"
msgctxt "invoice" msgctxt "invoice"
msgid "Remaining amount" msgid "Remaining amount"
msgstr "Data de pagamento" msgstr "Valor remanescente"
#: pretix/base/invoice.py:778 #: pretix/base/invoice.py:778
msgctxt "invoice" msgctxt "invoice"
@@ -3379,18 +3372,14 @@ msgid "Included taxes"
msgstr "Taxas incluídas" msgstr "Taxas incluídas"
#: pretix/base/invoice.py:838 #: pretix/base/invoice.py:838
#, fuzzy, python-brace-format #, python-brace-format
#| msgctxt "invoice"
#| msgid ""
#| "Using the conversion rate of 1:{rate} as published by the European "
#| "Central Bank on {date}, this corresponds to:"
msgctxt "invoice" msgctxt "invoice"
msgid "" msgid ""
"Using the conversion rate of 1:{rate} as published by the {authority} on " "Using the conversion rate of 1:{rate} as published by the {authority} on "
"{date}, this corresponds to:" "{date}, this corresponds to:"
msgstr "" msgstr ""
"Usando a taxa de conversão de 1: {rate} como publicado pelo Banco Central " "Usando a taxa de conversão de 1: {rate} como publicado pelo {authority} na "
"Europeu na {date}, isto corresponde á:" "{date}, isto corresponde a:"
#: pretix/base/invoice.py:853 #: pretix/base/invoice.py:853
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -4692,10 +4681,10 @@ msgid "Owned by ticket holder"
msgstr "Fatura para" msgstr "Fatura para"
#: pretix/base/models/giftcards.py:93 #: pretix/base/models/giftcards.py:93
#, fuzzy
#| msgid "The slug may only contain letters, numbers, dots and dashes."
msgid "The gift card code may only contain letters, numbers, dots and dashes." msgid "The gift card code may only contain letters, numbers, dots and dashes."
msgstr "A campo pode conter apenas letras, números, pontos e traços." msgstr ""
"O código do cartão-presente pode conter apenas letras, números, pontos e "
"traços."
#: pretix/base/models/giftcards.py:105 #: pretix/base/models/giftcards.py:105
#: pretix/control/templates/pretixcontrol/organizers/giftcard.html:39 #: pretix/control/templates/pretixcontrol/organizers/giftcard.html:39
@@ -4704,10 +4693,8 @@ msgid "Special terms and conditions"
msgstr "Termos e condições especiais" msgstr "Termos e condições especiais"
#: pretix/base/models/giftcards.py:219 pretix/base/models/giftcards.py:223 #: pretix/base/models/giftcards.py:219 pretix/base/models/giftcards.py:223
#, fuzzy
#| msgid "Category description"
msgid "Manual transaction" msgid "Manual transaction"
msgstr "Descrição da categoria" msgstr "Transação manual"
#: pretix/base/models/invoices.py:185 #: pretix/base/models/invoices.py:185
#, python-format #, python-format
@@ -4752,10 +4739,8 @@ msgstr ""
#: pretix/base/models/items.py:114 pretix/base/models/items.py:159 #: pretix/base/models/items.py:114 pretix/base/models/items.py:159
#: pretix/control/forms/item.py:99 #: pretix/control/forms/item.py:99
#, fuzzy
#| msgid "Product category"
msgid "Normal category" msgid "Normal category"
msgstr "Categoria de produtos" msgstr "Categoria normal"
#: pretix/base/models/items.py:115 pretix/control/forms/item.py:112 #: pretix/base/models/items.py:115 pretix/control/forms/item.py:112
msgid "Normal + cross-selling category" msgid "Normal + cross-selling category"
@@ -4811,10 +4796,8 @@ msgid "Add-on category"
msgstr "Categoria de produtos" msgstr "Categoria de produtos"
#: pretix/base/models/items.py:222 pretix/base/models/items.py:278 #: pretix/base/models/items.py:222 pretix/base/models/items.py:278
#, fuzzy
#| msgid "Optional. No products will be sold before this date."
msgid "Disable product for this date" msgid "Disable product for this date"
msgstr "Opcional. Nenhum produto será vendido antes desta data." msgstr "Desabilitar produto para esta data"
#: pretix/base/models/items.py:226 pretix/base/models/items.py:282 #: pretix/base/models/items.py:226 pretix/base/models/items.py:282
#: pretix/base/models/items.py:564 #: pretix/base/models/items.py:564
@@ -4827,23 +4810,16 @@ msgid "This product will not be sold after the given date."
msgstr "Este produto não será vendido após a data indicada." msgstr "Este produto não será vendido após a data indicada."
#: pretix/base/models/items.py:436 #: pretix/base/models/items.py:436
#, fuzzy
#| msgid "Event start date"
msgid "Event validity (default)" msgid "Event validity (default)"
msgstr "Data de início do evento" msgstr "Validade do evento (padrão)"
#: pretix/base/models/items.py:437 #: pretix/base/models/items.py:437
#, fuzzy
#| msgctxt "refund_source"
#| msgid "Customer"
msgid "Fixed time frame" msgid "Fixed time frame"
msgstr "Cliente" msgstr "Prazo fixo"
#: pretix/base/models/items.py:438 #: pretix/base/models/items.py:438
#, fuzzy
#| msgid "Gift card"
msgid "Dynamic validity" msgid "Dynamic validity"
msgstr "Cartão Presente" msgstr "Validade dinâmica"
#: pretix/base/models/items.py:444 pretix/control/forms/item.py:660 #: pretix/base/models/items.py:444 pretix/control/forms/item.py:660
#: pretix/control/templates/pretixcontrol/subevents/fragment_unavail_mode_indicator.html:3 #: pretix/control/templates/pretixcontrol/subevents/fragment_unavail_mode_indicator.html:3
@@ -4921,10 +4897,8 @@ msgstr ""
"que são comprados como complemento para outros produtos." "que são comprados como complemento para outros produtos."
#: pretix/base/models/items.py:512 pretix/base/models/items.py:1175 #: pretix/base/models/items.py:512 pretix/base/models/items.py:1175
#, fuzzy
#| msgid "Default price"
msgid "Suggested price" msgid "Suggested price"
msgstr "Preço padrão" msgstr "Preço sugerido"
#: pretix/base/models/items.py:513 pretix/base/models/items.py:1176 #: pretix/base/models/items.py:513 pretix/base/models/items.py:1176
msgid "" msgid ""
@@ -4944,10 +4918,8 @@ msgstr ""
"Quer comprar este produto ou não permitir que uma pessoa entre no seu evento" "Quer comprar este produto ou não permitir que uma pessoa entre no seu evento"
#: pretix/base/models/items.py:532 #: pretix/base/models/items.py:532
#, fuzzy
#| msgid "Is an admission ticket"
msgid "Is a personalized ticket" msgid "Is a personalized ticket"
msgstr "É um bilhete de admissão" msgstr "É um ingresso personalizado"
#: pretix/base/models/items.py:534 #: pretix/base/models/items.py:534
#, fuzzy #, fuzzy
@@ -5178,10 +5150,8 @@ msgid "Membership duration in months"
msgstr "Duração da assinatura em meses" msgstr "Duração da assinatura em meses"
#: pretix/base/models/items.py:728 #: pretix/base/models/items.py:728
#, fuzzy
#| msgid "Valid until"
msgid "Validity" msgid "Validity"
msgstr "Válido até" msgstr "Validade"
#: pretix/base/models/items.py:730 #: pretix/base/models/items.py:730
msgid "" msgid ""
@@ -5205,16 +5175,12 @@ msgstr ""
"alteração, mas manterão a validade atual." "alteração, mas manterão a validade atual."
#: pretix/base/models/items.py:738 pretix/control/forms/item.py:728 #: pretix/base/models/items.py:738 pretix/control/forms/item.py:728
#, fuzzy
#| msgid "Gift card"
msgid "Start of validity" msgid "Start of validity"
msgstr "Cartão Presente" msgstr "Início da validade"
#: pretix/base/models/items.py:739 #: pretix/base/models/items.py:739
#, fuzzy
#| msgid "End of presale"
msgid "End of validity" msgid "End of validity"
msgstr "Fim a pré venda" msgstr "Fim da validade"
#: pretix/base/models/items.py:742 #: pretix/base/models/items.py:742
msgid "Minutes" msgid "Minutes"
@@ -5369,10 +5335,8 @@ msgid "This is shown below the variation name in lists."
msgstr "Isso é mostrado abaixo do nome da variação nas listas." msgstr "Isso é mostrado abaixo do nome da variação nas listas."
#: pretix/base/models/items.py:1182 #: pretix/base/models/items.py:1182
#, fuzzy
#| msgid "New order requires approval"
msgid "Require approval" msgid "Require approval"
msgstr "Novo pedido precisa ser aprovado" msgstr "Requer aprovação"
#: pretix/base/models/items.py:1184 #: pretix/base/models/items.py:1184
#, fuzzy #, fuzzy
@@ -5399,16 +5363,12 @@ msgid "Membership types"
msgstr "Tipos de assinaturas" msgstr "Tipos de assinaturas"
#: pretix/base/models/items.py:1206 #: pretix/base/models/items.py:1206
#, fuzzy
#| msgid "This product will not be sold before the given date."
msgid "This variation will not be sold before the given date." msgid "This variation will not be sold before the given date."
msgstr "Este produto não será vendido antes da data indicada." msgstr "Esta variação não será vendida antes da data indicada."
#: pretix/base/models/items.py:1216 #: pretix/base/models/items.py:1216
#, fuzzy
#| msgid "This product will not be sold after the given date."
msgid "This variation will not be sold after the given date." msgid "This variation will not be sold after the given date."
msgstr "Este produto não será vendido após a data indicada." msgstr "Esta variação não será vendida após a data indicada."
#: pretix/base/models/items.py:1224 #: pretix/base/models/items.py:1224
msgid "Sell on all sales channels the product is sold on" msgid "Sell on all sales channels the product is sold on"
@@ -5474,10 +5434,8 @@ msgstr ""
"os add-ons normalmente custem dinheiro individualmente." "os add-ons normalmente custem dinheiro individualmente."
#: pretix/base/models/items.py:1476 #: pretix/base/models/items.py:1476
#, fuzzy
#| msgid "Allow product to be canceled"
msgid "Allow the same product to be selected multiple times" msgid "Allow the same product to be selected multiple times"
msgstr "Permitir que o produto seja cancelado" msgstr "Permitir que o mesmo produto seja selecionado múltiplas vezes"
#: pretix/base/models/items.py:1495 #: pretix/base/models/items.py:1495
msgid "The add-on's category must belong to the same event as the item." msgid "The add-on's category must belong to the same event as the item."
@@ -11440,6 +11398,8 @@ msgid ""
"By clicking \"Accept all cookies\", you agree to the storing of cookies and " "By clicking \"Accept all cookies\", you agree to the storing of cookies and "
"use of similar technologies on your device." "use of similar technologies on your device."
msgstr "" msgstr ""
"Ao clicar em \"Aceitar todos os cookies\", você concorda em armazenar "
"cookies e utilizar tecnologias similares em seu dispositivo."
#: pretix/base/settings.py:3311 #: pretix/base/settings.py:3311
#, fuzzy #, fuzzy
@@ -11461,10 +11421,8 @@ msgid "Secondary dialog text"
msgstr "" msgstr ""
#: pretix/base/settings.py:3332 #: pretix/base/settings.py:3332
#, fuzzy
#| msgid "Payment date"
msgid "Privacy settings" msgid "Privacy settings"
msgstr "Data de pagamento" msgstr "Preferências de privacidade"
#: pretix/base/settings.py:3337 #: pretix/base/settings.py:3337
msgid "Dialog title" msgid "Dialog title"
@@ -11472,7 +11430,7 @@ msgstr ""
#: pretix/base/settings.py:3343 #: pretix/base/settings.py:3343
msgid "Accept all cookies" msgid "Accept all cookies"
msgstr "" msgstr "Aceitar todos os cookies"
#: pretix/base/settings.py:3348 #: pretix/base/settings.py:3348
#, fuzzy #, fuzzy
@@ -11520,12 +11478,12 @@ msgstr ""
#: pretix/base/settings.py:3429 #: pretix/base/settings.py:3429
msgctxt "person_name_salutation" msgctxt "person_name_salutation"
msgid "Ms" msgid "Ms"
msgstr "" msgstr "Sra"
#: pretix/base/settings.py:3430 #: pretix/base/settings.py:3430
msgctxt "person_name_salutation" msgctxt "person_name_salutation"
msgid "Mr" msgid "Mr"
msgstr "" msgstr "Sr"
#: pretix/base/settings.py:3431 #: pretix/base/settings.py:3431
msgctxt "person_name_salutation" msgctxt "person_name_salutation"
@@ -11559,7 +11517,7 @@ msgstr "Sobrenome"
#: pretix/base/settings.py:3687 #: pretix/base/settings.py:3687
msgctxt "person_name_sample" msgctxt "person_name_sample"
msgid "John" msgid "John"
msgstr "" msgstr "João"
#: pretix/base/settings.py:3469 pretix/base/settings.py:3485 #: pretix/base/settings.py:3469 pretix/base/settings.py:3485
#: pretix/base/settings.py:3501 pretix/base/settings.py:3517 #: pretix/base/settings.py:3501 pretix/base/settings.py:3517
@@ -11568,27 +11526,25 @@ msgstr ""
#: pretix/base/settings.py:3657 pretix/base/settings.py:3688 #: pretix/base/settings.py:3657 pretix/base/settings.py:3688
msgctxt "person_name_sample" msgctxt "person_name_sample"
msgid "Doe" msgid "Doe"
msgstr "" msgstr "Silva"
#: pretix/base/settings.py:3475 pretix/base/settings.py:3491 #: pretix/base/settings.py:3475 pretix/base/settings.py:3491
#: pretix/base/settings.py:3523 pretix/base/settings.py:3642 #: pretix/base/settings.py:3523 pretix/base/settings.py:3642
#: pretix/base/settings.py:3664 #: pretix/base/settings.py:3664
msgctxt "person_name" msgctxt "person_name"
msgid "Title" msgid "Title"
msgstr "" msgstr "Título"
#: pretix/base/settings.py:3483 pretix/base/settings.py:3499 #: pretix/base/settings.py:3483 pretix/base/settings.py:3499
#: pretix/base/settings.py:3532 pretix/base/settings.py:3655 #: pretix/base/settings.py:3532 pretix/base/settings.py:3655
#: pretix/base/settings.py:3686 #: pretix/base/settings.py:3686
msgctxt "person_name_sample" msgctxt "person_name_sample"
msgid "Dr" msgid "Dr"
msgstr "" msgstr "Dr"
#: pretix/base/settings.py:3507 pretix/base/settings.py:3524 #: pretix/base/settings.py:3507 pretix/base/settings.py:3524
#, fuzzy
#| msgid "Device name"
msgid "First name" msgid "First name"
msgstr "Nome do dispositivo" msgstr "Nome"
#: pretix/base/settings.py:3508 pretix/base/settings.py:3525 #: pretix/base/settings.py:3508 pretix/base/settings.py:3525
#, fuzzy #, fuzzy
@@ -11600,7 +11556,7 @@ msgstr "Nome do dispositivo"
#: pretix/control/forms/organizer.py:651 #: pretix/control/forms/organizer.py:651
msgctxt "person_name_sample" msgctxt "person_name_sample"
msgid "John Doe" msgid "John Doe"
msgstr "" msgstr "João Silva"
#: pretix/base/settings.py:3595 #: pretix/base/settings.py:3595
#, fuzzy #, fuzzy
@@ -11627,7 +11583,7 @@ msgstr "Cancelamento"
#: pretix/base/settings.py:3685 #: pretix/base/settings.py:3685
msgctxt "person_name_sample" msgctxt "person_name_sample"
msgid "Mr" msgid "Mr"
msgstr "" msgstr "Sr"
#: pretix/base/settings.py:3667 #: pretix/base/settings.py:3667
msgctxt "person_name" msgctxt "person_name"
@@ -18175,7 +18131,7 @@ msgstr "Organizadores"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html:29 #: pretix/control/templates/pretixcontrol/email_setup_simple.html:29
msgid "This is the SPF record we found on your domain:" msgid "This is the SPF record we found on your domain:"
msgstr "" msgstr "Este é o registro SPF encontrado em seu domínio:"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html:33 #: pretix/control/templates/pretixcontrol/email_setup_simple.html:33
msgid "To fix this, include the following part before the last word:" msgid "To fix this, include the following part before the last word:"
@@ -24293,7 +24249,7 @@ msgstr "Produto"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:264 #: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:264
#: pretix/control/templates/pretixcontrol/subevents/detail.html:124 #: pretix/control/templates/pretixcontrol/subevents/detail.html:124
msgid "Add a new quota" msgid "Add a new quota"
msgstr "" msgstr "Adicionar uma quota nova"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:485 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:485
#: pretix/control/templates/pretixcontrol/subevents/detail.html:128 #: pretix/control/templates/pretixcontrol/subevents/detail.html:128

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-09-16 13:00+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Svyatoslav <slava@digitalarthouse.eu>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
"ru/>\n" "ru/>\n"
"Language: ru\n" "Language: ru\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.7\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -32414,12 +32414,12 @@ msgid "Current value:"
msgstr "Валюта" msgstr "Валюта"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463
#, python-format #, fuzzy, python-format
msgid "One product" msgid "One product"
msgid_plural "%(num)s products" msgid_plural "%(num)s products"
msgstr[0] "Один продукт" msgstr[0] "Один продуктДополнительные продуктыМного продуктов"
msgstr[1] "Дополнительные продукты" msgstr[1] ""
msgstr[2] "Много продуктов" msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477
#, fuzzy, python-format #, fuzzy, python-format

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-09-15 18:00+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Kristian Feldsam <feldsam@gmail.com>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Slovak <https://translate.pretix.eu/projects/pretix/pretix/sk/" "Language-Team: Slovak <https://translate.pretix.eu/projects/pretix/pretix/sk/"
">\n" ">\n"
"Language: sk\n" "Language: sk\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 5.7\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -15412,7 +15412,7 @@ msgid "The order has been changed:"
msgstr "Poradie bolo zmenené:" msgstr "Poradie bolo zmenené:"
#: pretix/control/logdisplay.py:99 #: pretix/control/logdisplay.py:99
#, python-brace-format #, fuzzy, python-brace-format
msgid "" msgid ""
"Position #{posid}: {old_item} ({old_price}) changed to {new_item} " "Position #{posid}: {old_item} ({old_price}) changed to {new_item} "
"({new_price})." "({new_price})."
@@ -15515,7 +15515,7 @@ msgid "A block has been removed for position #{posid}."
msgstr "Blok bol odstránený pre pozíciu #{posid}." msgstr "Blok bol odstránený pre pozíciu #{posid}."
#: pretix/control/logdisplay.py:285 #: pretix/control/logdisplay.py:285
#, python-brace-format #, fuzzy, python-brace-format
msgid "" msgid ""
"Position #{posid} ({old_item}, {old_price}) split into new order: {order}" "Position #{posid} ({old_item}, {old_price}) split into new order: {order}"
msgstr "" msgstr ""

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2024-05-23 14:03+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Serhii Horichenko <m@sgg.im>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Ukrainian <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Ukrainian <https://translate.pretix.eu/projects/pretix/pretix/"
"uk/>\n" "uk/>\n"
"Language: uk\n" "Language: uk\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.5.5\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -32502,26 +32502,26 @@ msgstr ""
"відповідно змініть налаштування свого браузера." "відповідно змініть налаштування свого браузера."
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:17 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:17
#, python-format #, fuzzy, python-format
msgid "You need to choose exactly one option from this category." msgid "You need to choose exactly one option from this category."
msgid_plural "You need to choose %(min_count)s options from this category." msgid_plural "You need to choose %(min_count)s options from this category."
msgstr[0] "" msgstr[0] ""
"Однина\n" "Однина\n"
"Вам необхідно обрати одну опцію з цієї категорії." "Вам необхідно обрати одну опцію з цієї категорії.Множина\n"
msgstr[1] "" "Вам обрати %(min_count)s опції з цієї категорії.Множина\n"
"Множина\n"
"Вам обрати %(min_count)s опції з цієї категорії."
msgstr[2] ""
"Множина\n"
"Вам обрати %(min_count)s опцій з цієї категорії." "Вам обрати %(min_count)s опцій з цієї категорії."
msgstr[1] ""
msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26
#, python-format #, fuzzy, python-format
msgid "You can choose %(max_count)s option from this category." msgid "You can choose %(max_count)s option from this category."
msgid_plural "You can choose up to %(max_count)s options from this category." msgid_plural "You can choose up to %(max_count)s options from this category."
msgstr[0] "Ви можете обрати одну опцію з цієї категорії." msgstr[0] ""
msgstr[1] "Ви можете обрати %(max_count)s опції з цієї категорії." "Ви можете обрати одну опцію з цієї категорії.Ви можете обрати %(max_count)s "
msgstr[2] "Ви можете обрати %(max_count)s опцій з цієї категорії." "опції з цієї категорії.Ви можете обрати %(max_count)s опцій з цієї категорії."
msgstr[1] ""
msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34
#, python-format #, python-format
@@ -32799,12 +32799,14 @@ msgid "Usage:"
msgstr "Використання:" msgstr "Використання:"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:138 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:138
#, python-format #, fuzzy, python-format
msgid "This ticket has been used once." msgid "This ticket has been used once."
msgid_plural "This ticket has been used %(count)s times." msgid_plural "This ticket has been used %(count)s times."
msgstr[0] "Цей квиток було використано один раз." msgstr[0] ""
msgstr[1] "Цей квиток було використано кілька разів." "Цей квиток було використано один раз.Цей квиток було використано кілька "
msgstr[2] "Цей квиток було використано %(count)s разів." "разів.Цей квиток було використано %(count)s разів."
msgstr[1] ""
msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:170 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:170
msgid "No attendee name provided" msgid "No attendee name provided"
@@ -32891,12 +32893,12 @@ msgid "Current value:"
msgstr "Поточна вартість" msgstr "Поточна вартість"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:463
#, python-format #, fuzzy, python-format
msgid "One product" msgid "One product"
msgid_plural "%(num)s products" msgid_plural "%(num)s products"
msgstr[0] "Один продукт" msgstr[0] "Один продукт%(num)s продукти%(num)s продуктів"
msgstr[1] "%(num)s продукти" msgstr[1] ""
msgstr[2] "%(num)s продуктів" msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:477
#, python-format #, python-format

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-29 13:18+0000\n" "POT-Creation-Date: 2025-01-29 13:18+0000\n"
"PO-Revision-Date: 2025-01-31 01:00+0000\n" "PO-Revision-Date: 2025-02-21 19:00+0000\n"
"Last-Translator: Chislon <chislon@gmail.com>\n" "Last-Translator: anonymous <noreply@weblate.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/" "Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/"
"projects/pretix/pretix/zh_Hant/>\n" "projects/pretix/pretix/zh_Hant/>\n"
"Language: zh_Hant\n" "Language: zh_Hant\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.9.2\n" "X-Generator: Weblate 5.10\n"
#: pretix/_base_settings.py:87 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -19302,7 +19302,7 @@ msgstr "是否確實要刪除商品<strong>%(item)s</strong>"
#: pretix/control/templates/pretixcontrol/item/delete.html:22 #: pretix/control/templates/pretixcontrol/item/delete.html:22
#: pretix/control/templates/pretixcontrol/items/quota_delete.html:24 #: pretix/control/templates/pretixcontrol/items/quota_delete.html:24
#, python-format #, fuzzy, python-format
msgid "That will cause %(count)s voucher to be unusable." msgid "That will cause %(count)s voucher to be unusable."
msgid_plural "That will cause %(count)s voucher to be unusable." msgid_plural "That will cause %(count)s voucher to be unusable."
msgstr[0] "這將導致%(count)s的優惠券不可用.這將導致 %(count)s的優惠券不可用." msgstr[0] "這將導致%(count)s的優惠券不可用.這將導致 %(count)s的優惠券不可用."
@@ -24625,7 +24625,7 @@ msgid "Soon"
msgstr "很快地" msgstr "很快地"
#: pretix/control/views/dashboards.py:561 #: pretix/control/views/dashboards.py:561
#, python-brace-format #, fuzzy, python-brace-format
msgid "{num} order" msgid "{num} order"
msgid_plural "{num} orders" msgid_plural "{num} orders"
msgstr[0] "" msgstr[0] ""
@@ -25135,7 +25135,7 @@ msgid_plural "Your invoices"
msgstr[0] "你的發票" msgstr[0] "你的發票"
#: pretix/control/views/orders.py:537 #: pretix/control/views/orders.py:537
#, python-brace-format #, fuzzy, python-brace-format
msgid "" msgid ""
"Hello,\n" "Hello,\n"
"\n" "\n"
@@ -28600,7 +28600,7 @@ msgid "on {date} at {time}"
msgstr "在{date}在{time}" msgstr "在{date}在{time}"
#: pretix/plugins/sendmail/models.py:340 #: pretix/plugins/sendmail/models.py:340
#, python-format #, fuzzy, python-format
msgid "%(count)d day after event end at %(time)s" msgid "%(count)d day after event end at %(time)s"
msgid_plural "%(count)d days after event end at %(time)s" msgid_plural "%(count)d days after event end at %(time)s"
msgstr[0] "" msgstr[0] ""
@@ -28620,11 +28620,10 @@ msgid_plural "%(count)d days after event start at %(time)s"
msgstr[0] "%(count)d 事件在%(time)s開始後的一天" msgstr[0] "%(count)d 事件在%(time)s開始後的一天"
#: pretix/plugins/sendmail/models.py:368 #: pretix/plugins/sendmail/models.py:368
#, python-format #, fuzzy, python-format
msgid "%(count)d day before event start at %(time)s" msgid "%(count)d day before event start at %(time)s"
msgid_plural "%(count)d days before event start at %(time)s" msgid_plural "%(count)d days before event start at %(time)s"
msgstr[0] "" msgstr[0] "%(count)d 事件在%(time)s開始的前一天 , 事件在%(time)s開始前的%(count)d天"
"%(count)d 事件在%(time)s開始的前一天 , 事件在%(time)s開始前的%(count)d天"
#: pretix/plugins/sendmail/signals.py:101 #: pretix/plugins/sendmail/signals.py:101
msgid "Scheduled emails" msgid "Scheduled emails"
@@ -28873,7 +28872,7 @@ msgid ""
msgstr "通過等候名單功能,向當前等待接收優惠券的每個人發送電子郵件。" msgstr "通過等候名單功能,向當前等待接收優惠券的每個人發送電子郵件。"
#: pretix/plugins/sendmail/views.py:516 #: pretix/plugins/sendmail/views.py:516
#, python-format #, fuzzy, python-format
msgid "%(number)s waiting list entry" msgid "%(number)s waiting list entry"
msgid_plural "%(number)s waiting list entries" msgid_plural "%(number)s waiting list entries"
msgstr[0] "%(number)s的等待名單條目%(number)s的等待名單條目" msgstr[0] "%(number)s的等待名單條目%(number)s的等待名單條目"
@@ -30754,12 +30753,11 @@ msgstr[0] ""
"你需要從此類別中選擇一個選項。你需要從此類別中選擇%(min_count)s的選項。" "你需要從此類別中選擇一個選項。你需要從此類別中選擇%(min_count)s的選項。"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26
#, python-format #, fuzzy, python-format
msgid "You can choose %(max_count)s option from this category." msgid "You can choose %(max_count)s option from this category."
msgid_plural "You can choose up to %(max_count)s options from this category." msgid_plural "You can choose up to %(max_count)s options from this category."
msgstr[0] "" msgstr[0] "你可以從此類別中選擇%(max_count)s選項。你可以從此類別中選擇%(max_count)s選項"
"你可以從此類別中選擇%(max_count)s選項。你可以從此類別中選擇%(max_count)s選" ""
"項。"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34
#, python-format #, python-format

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,13 @@ DEFAULT_TICKET_LAYOUT = '''[
"content": "secret", "content": "secret",
"text": "", "text": "",
"text_i18n": {}, "text_i18n": {},
"nowhitespace": false "nowhitespace": false,
"color": [
0,
0,
0,
1
]
}, },
{ {
"type": "poweredby", "type": "poweredby",

View File

@@ -386,6 +386,11 @@ def get_or_create_cart_id(request, create=True):
if 'carts' in request.session: if 'carts' in request.session:
request.session['carts'][current_id] = {} request.session['carts'][current_id] = {}
else: else:
if 'widget_data' in request.GET:
try:
request.session['carts'][current_id]['widget_data'] = json.loads(request.GET.get('widget_data'))
except ValueError:
pass
return current_id return current_id
cart_data = {} cart_data = {}

View File

@@ -676,6 +676,8 @@ class SSOLoginView(RedirectBackMixin, View):
popup_origin = None popup_origin = None
nonce = get_random_string(32) nonce = get_random_string(32)
pkce_code_verifier = get_random_string(64)
request.session[f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'] = pkce_code_verifier
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true" request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
@@ -684,7 +686,7 @@ class SSOLoginView(RedirectBackMixin, View):
}) })
if self.provider.method == "oidc": if self.provider.method == "oidc":
return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri)) return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri, pkce_code_verifier))
else: else:
raise Http404("Unknown SSO method.") raise Http404("Unknown SSO method.")
@@ -718,6 +720,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
) )
return HttpResponseRedirect(redirect_to) return HttpResponseRedirect(redirect_to)
r = super().dispatch(request, *args, **kwargs) r = super().dispatch(request, *args, **kwargs)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier', None)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_nonce', None) request.session.pop(f'pretix_customerauth_{self.provider.pk}_nonce', None)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_popup_origin', None) request.session.pop(f'pretix_customerauth_{self.provider.pk}_popup_origin', None)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested', None) request.session.pop(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested', None)
@@ -763,6 +766,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
self.provider, self.provider,
request.GET.get('code'), request.GET.get('code'),
redirect_uri, redirect_uri,
request.session.get(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'),
) )
except ValidationError as e: except ValidationError as e:
for msg in e: for msg in e:

View File

@@ -72,6 +72,9 @@ We currently do not implement the following optional parts of the spec:
We also implement the Discovery extension (without issuer discovery) We also implement the Discovery extension (without issuer discovery)
as per https://openid.net/specs/openid-connect-discovery-1_0.html as per https://openid.net/specs/openid-connect-discovery-1_0.html
We also implement the PKCE extension for OAuth:
https://www.rfc-editor.org/rfc/rfc7636
The implementation passed the certification tests against the following profiles, but we did not The implementation passed the certification tests against the following profiles, but we did not
acquire formal certification: acquire formal certification:
@@ -136,19 +139,22 @@ class AuthorizeView(View):
self._construct_redirect_uri(redirect_uri, response_mode, qs) self._construct_redirect_uri(redirect_uri, response_mode, qs)
) )
def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce): def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
code_challenge, code_challenge_method):
form = AuthenticationForm(data=request.POST if "login-email" in request.POST else None, request=request, form = AuthenticationForm(data=request.POST if "login-email" in request.POST else None, request=request,
prefix="login") prefix="login")
if "login-email" in request.POST and form.is_valid(): if "login-email" in request.POST and form.is_valid():
customer_login(request, form.get_customer()) customer_login(request, form.get_customer())
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, form.get_customer()) return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce,
code_challenge, code_challenge_method, form.get_customer())
else: else:
return render(request, 'pretixpresale/organizers/customer_login.html', { return render(request, 'pretixpresale/organizers/customer_login.html', {
'providers': [], 'providers': [],
'form': form, 'form': form,
}) })
def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, customer): def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
code_challenge_method, customer):
response_type = response_type.split(' ') response_type = response_type.split(' ')
qs = {} qs = {}
id_token_kwargs = {} id_token_kwargs = {}
@@ -162,6 +168,8 @@ class AuthorizeView(View):
expires=now() + timedelta(minutes=10), expires=now() + timedelta(minutes=10),
auth_time=get_customer_auth_time(self.request), auth_time=get_customer_auth_time(self.request),
nonce=nonce, nonce=nonce,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method,
) )
qs['code'] = grant.code qs['code'] = grant.code
id_token_kwargs['with_code'] = grant.code id_token_kwargs['with_code'] = grant.code
@@ -209,6 +217,8 @@ class AuthorizeView(View):
prompt = request_data.get("prompt") prompt = request_data.get("prompt")
response_type = request_data.get("response_type") response_type = request_data.get("response_type")
scope = request_data.get("scope", "").split(" ") scope = request_data.get("scope", "").split(" ")
code_challenge = request_data.get("code_challenge")
code_challenge_method = request_data.get("code_challenge_method")
if not client_id: if not client_id:
return self._final_error("invalid_request", "client_id missing") return self._final_error("invalid_request", "client_id missing")
@@ -247,6 +257,16 @@ class AuthorizeView(View):
return self._redirect_error("invalid_request", "id_token_hint currently not supported by this server", return self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
redirect_uri, response_mode, state) redirect_uri, response_mode, state)
if code_challenge and code_challenge_method != "S256":
# "Clients re permitted to use "plain" only if they cannot support "S256" for some technical reason and
# know via out-of-band configuration that the S256 MUST be implemented, plain is not mandatory."
return self._redirect_error("invalid_request", "code_challenge transform algorithm not supported",
redirect_uri, response_mode, state)
if client.require_pkce and not code_challenge:
return self._redirect_error("invalid_request", "code_challenge (PKCE) required",
redirect_uri, response_mode, state)
has_valid_session = bool(request.customer) has_valid_session = bool(request.customer)
if has_valid_session and max_age: if has_valid_session and max_age:
try: try:
@@ -262,9 +282,11 @@ class AuthorizeView(View):
has_valid_session = False has_valid_session = False
if has_valid_session: if has_valid_session:
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, request.customer) return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
code_challenge_method, request.customer)
else: else:
return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce) return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
code_challenge, code_challenge_method)
class TokenView(View): class TokenView(View):
@@ -362,6 +384,24 @@ class TokenView(View):
"error_description": "Mismatch of redirect_uri" "error_description": "Mismatch of redirect_uri"
}, status=400) }, status=400)
if grant.code_challenge:
if not request.POST.get("code_verifier"):
return JsonResponse({
"error": "invalid_grant",
"error_description": "Missing of code_verifier"
}, status=400)
if grant.code_challenge_method == "S256":
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
print(grant.code_challenge, expected_challenge)
if expected_challenge != grant.code_challenge:
return JsonResponse({
"error": "invalid_grant",
"error_description": "Mismatch of code_verifier with code_challenge"
}, status=400)
else:
raise ValueError("Unsupported code_challenge_method in database")
with transaction.atomic(): with transaction.atomic():
token = self.client.access_tokens.create( token = self.client.access_tokens.create(
customer=grant.customer, customer=grant.customer,
@@ -503,6 +543,7 @@ class ConfigurationView(View):
'token_endpoint_auth_methods_supported': [ 'token_endpoint_auth_methods_supported': [
'client_secret_post', 'client_secret_basic' 'client_secret_post', 'client_secret_basic'
], ],
'code_challenge_methods_supported': ['S256'],
'claims_supported': [ 'claims_supported': [
'iss', 'iss',
'aud', 'aud',

View File

@@ -313,6 +313,7 @@ var editor = {
content: o.content, content: o.content,
}); });
} else if (o.type === "barcodearea") { } else if (o.type === "barcodearea") {
col = (new fabric.Color(o.fill))._source;
d.push({ d.push({
type: "barcodearea", type: "barcodearea",
page: editor.pdf_page_number, page: editor.pdf_page_number,
@@ -323,6 +324,7 @@ var editor = {
text: o.text, text: o.text,
text_i18n: o.text_i18n || {}, text_i18n: o.text_i18n || {},
nowhitespace: o.nowhitespace || false, nowhitespace: o.nowhitespace || false,
color: col,
}); });
} else if (o.type === "poweredby") { } else if (o.type === "poweredby") {
d.push({ d.push({
@@ -349,6 +351,10 @@ var editor = {
o.content = d.content; o.content = d.content;
o.scaleToHeight(editor._mm2px(d.size)); o.scaleToHeight(editor._mm2px(d.size));
o.nowhitespace = d.nowhitespace || false; o.nowhitespace = d.nowhitespace || false;
if (!d.color) {
d.color = [0, 0, 0, 1];
}
o.set('fill', 'rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
if (d.content === "other") { if (d.content === "other") {
o.text = d.text o.text = d.text
} else if (d.content === "other_i18n") { } else if (d.content === "other_i18n") {
@@ -640,6 +646,8 @@ var editor = {
})); }));
}); });
} else if (o.type === "barcodearea") { } else if (o.type === "barcodearea") {
var col = (new fabric.Color(o.fill))._source;
$("#toolbox-qrcolor").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
$("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2)); $("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2));
$("#toolbox-qrwhitespace").prop("checked", o.nowhitespace || false); $("#toolbox-qrwhitespace").prop("checked", o.nowhitespace || false);
} else if (o.type === "imagearea") { } else if (o.type === "imagearea") {
@@ -742,6 +750,7 @@ var editor = {
o.set('scaleX', 1); o.set('scaleX', 1);
o.set('scaleY', 1); o.set('scaleY', 1);
o.set('top', new_top) o.set('top', new_top)
o.set('fill', $("#toolbox-qrcolor").val());
o.nowhitespace = $("#toolbox-qrwhitespace").prop("checked") || false; o.nowhitespace = $("#toolbox-qrwhitespace").prop("checked") || false;
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other"); $("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
@@ -1028,7 +1037,7 @@ var editor = {
width: 100, width: 100,
height: 100, height: 100,
lockRotation: true, lockRotation: true,
fill: '#666', fill: '#000',
content: $(this).attr("data-content"), content: $(this).attr("data-content"),
text: '', text: '',
nowhitespace: true, nowhitespace: true,

View File

@@ -31,7 +31,16 @@ $(function () {
var show_dialog = !storage_val; var show_dialog = !storage_val;
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]"); var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = $("#cookie-consent-modal"); var consent_modal = $("#cookie-consent-modal");
if (storage_val) { var widget_consent = $("#cookie-consent-from-widget").text();
if (widget_consent) {
widget_consent = JSON.parse(widget_consent);
storage_val = {}
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1;
})
show_dialog = false;
$("#cookie-consent-reopen").hide();
} else if (storage_val) {
storage_val = JSON.parse(storage_val); storage_val = JSON.parse(storage_val);
consent_checkboxes.each(function () { consent_checkboxes.each(function () {
if (typeof storage_val[this.name] === "undefined") { if (typeof storage_val[this.name] === "undefined") {
@@ -43,14 +52,6 @@ $(function () {
}) })
} else { } else {
storage_val = {} storage_val = {}
var consented = $("#cookie-consent-from-widget").text();
if (consented) {
consented = JSON.parse(consented);
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = consented.indexOf(this.name) > -1;
})
show_dialog = false
}
} }
update_consent(storage_val); update_consent(storage_val);

View File

@@ -80,6 +80,17 @@
"nowhitespace": { "nowhitespace": {
"description": "Whether a barcode should be rendered without margins. Only used for type 'barcodearea'.", "description": "Whether a barcode should be rendered without margins. Only used for type 'barcodearea'.",
"type": "boolean" "type": "boolean"
},
"color": {
"description": "QR color as a tuple of three integers in the 0-255 range and one float in the 0-1 range. The last value (alpha) is ignored by the current implementation but might be used in the future.",
"type": "array",
"items": {
"type": "number",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 4
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@@ -236,7 +236,8 @@ def provider(organizer):
"response_modes_supported": ["query"], "response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code"], "grant_types_supported": ["authorization_code"],
"scopes_supported": ["openid", "email", "profile"], "scopes_supported": ["openid", "email", "profile"],
"claims_supported": ["email", "sub"] "claims_supported": ["email", "sub"],
"code_challenge_methods_supported": ["plain", "S256"],
} }
} }
) )
@@ -244,6 +245,21 @@ def provider(organizer):
@pytest.mark.django_db @pytest.mark.django_db
def test_authorize_url(provider): def test_authorize_url(provider):
assert (
"https://example.com/authorize?"
"response_type=code&"
"client_id=abc123&"
"scope=openid+email+profile&"
"state=state_val&"
"redirect_uri=https%3A%2F%2Fredirect%3Ffoo%3Dbar&"
"code_challenge=S1ZnvzwMZHrWOO62nENdJ6jhODhf7VfyZFBIXQyrTKo&"
"code_challenge_method=S256"
) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar", "pkce_value")
@pytest.mark.django_db
def test_authorize_url_no_pkce(provider):
del provider.configuration["provider_config"]["code_challenge_methods_supported"]
assert ( assert (
"https://example.com/authorize?" "https://example.com/authorize?"
"response_type=code&" "response_type=code&"
@@ -251,7 +267,7 @@ def test_authorize_url(provider):
"scope=openid+email+profile&" "scope=openid+email+profile&"
"state=state_val&" "state=state_val&"
"redirect_uri=https%3A%2F%2Fredirect%3Ffoo%3Dbar" "redirect_uri=https%3A%2F%2Fredirect%3Ffoo%3Dbar"
) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar") ) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar", "pkce_value")
@pytest.mark.django_db @pytest.mark.django_db
@@ -264,7 +280,7 @@ def test_validate_authorization_invalid(provider):
status=400, status=400,
) )
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar") oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")
@pytest.mark.django_db @pytest.mark.django_db
@@ -281,6 +297,7 @@ def test_validate_authorization_userinfo_invalid(provider):
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": "code_received", "code": "code_received",
"redirect_uri": "https://redirect?foo=bar", "redirect_uri": "https://redirect?foo=bar",
"code_verifier": "pkce_value",
}) })
], ],
) )
@@ -296,7 +313,7 @@ def test_validate_authorization_userinfo_invalid(provider):
], ],
) )
with pytest.raises(ValidationError) as e: with pytest.raises(ValidationError) as e:
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar") oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")
assert 'could not fetch' in str(e.value) assert 'could not fetch' in str(e.value)
@@ -314,6 +331,7 @@ def test_validate_authorization_valid(provider):
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": "code_received", "code": "code_received",
"redirect_uri": "https://redirect?foo=bar", "redirect_uri": "https://redirect?foo=bar",
"code_verifier": "pkce_value",
}) })
], ],
) )
@@ -328,4 +346,4 @@ def test_validate_authorization_valid(provider):
matchers.header_matcher({"Authorization": "Bearer test_access_token"}) matchers.header_matcher({"Authorization": "Bearer test_access_token"})
], ],
) )
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar") oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")

View File

@@ -1685,7 +1685,7 @@ class CartTest(CartTestMixin, TestCase):
def test_voucher_free_price_lower_bound(self): def test_voucher_free_price_lower_bound(self):
with scopes_disabled(): with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event) v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
self.ticket.free_price = False self.ticket.free_price = True
self.ticket.save() self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1', 'item_%d' % self.ticket.id: '1',
@@ -1708,6 +1708,70 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(objs[0].listed_price, Decimal('23.00')) self.assertEqual(objs[0].listed_price, Decimal('23.00'))
self.assertEqual(objs[0].price_after_voucher, Decimal('20.70')) self.assertEqual(objs[0].price_after_voucher, Decimal('20.70'))
def test_voucher_free_price_redeem_lowers_if_min(self):
with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
self.ticket.free_price = True
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '23.00',
}, follow=True)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('23.00'))
self.assertEqual(objs[0].listed_price, Decimal('23.00'))
self.assertEqual(objs[0].price_after_voucher, Decimal('23.00'))
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug),
{'voucher': v.code},
follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
self.assertIn('20.70', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('20.70', doc.select('.cart .cart-row')[0].select('.price')[1].text)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('20.70'))
self.assertEqual(objs[0].listed_price, Decimal('23.00'))
self.assertEqual(objs[0].price_after_voucher, Decimal('20.70'))
def test_voucher_free_price_redeem_keeps_if_not_min(self):
with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
self.ticket.free_price = True
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '25.00',
}, follow=True)
response = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug),
{'voucher': v.code},
follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('We did not find any position in your cart that we could use this voucher for', doc.select('.alert-danger')[0].text)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('25.00'))
self.assertEqual(objs[0].listed_price, Decimal('23.00'))
self.assertEqual(objs[0].price_after_voucher, Decimal('23.00'))
def test_voucher_redemed(self): def test_voucher_redemed(self):
with scopes_disabled(): with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, redeemed=1) v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, redeemed=1)

View File

@@ -208,6 +208,37 @@ def test_authorize_with_prompt_none(env, client, ssoclient):
assert re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location']) assert re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location'])
@pytest.mark.django_db
def test_authorize_with_invalid_pkce_method(env, client, ssoclient):
url = f'/bigevents/oauth2/v1/authorize?' \
f'client_id={ssoclient[0].client_id}&' \
f'redirect_uri=https://example.net&' \
f'response_type=code&state=STATE&scope=openid+profile&' \
f'code_challenge=pkce_value&code_challenge_method=plain'
r = client.get(url)
assert r.status_code == 302
assert r.headers['Location'] == 'https://example.net?' \
'error=invalid_request&' \
'error_description=code_challenge+transform+algorithm+not+supported&' \
'state=STATE'
@pytest.mark.django_db
def test_authorize_with_missing_pkce_if_required(env, client, ssoclient):
ssoclient[0].require_pkce = True
ssoclient[0].save()
url = f'/bigevents/oauth2/v1/authorize?' \
f'client_id={ssoclient[0].client_id}&' \
f'redirect_uri=https://example.net&' \
f'response_type=code&state=STATE&scope=openid+profile'
r = client.get(url)
assert r.status_code == 302
assert r.headers['Location'] == 'https://example.net?' \
'error=invalid_request&' \
'error_description=code_challenge+%28PKCE%29+required&' \
'state=STATE'
@pytest.mark.django_db @pytest.mark.django_db
def test_authorize_require_login_if_prompt_requires_it_or_is_expired(env, client, ssoclient): def test_authorize_require_login_if_prompt_requires_it_or_is_expired(env, client, ssoclient):
with freeze_time("2021-04-10T11:00:00+02:00"): with freeze_time("2021-04-10T11:00:00+02:00"):
@@ -286,7 +317,7 @@ def test_token_require_client_id(env, client, ssoclient):
assert b'unsupported_grant_type' in r.content assert b'unsupported_grant_type' in r.content
def _authorization_step(client, ssoclient): def _authorization_step(client, ssoclient, code_challenge=None):
r = client.post('/bigevents/account/login', { r = client.post('/bigevents/account/login', {
'email': 'john@example.org', 'email': 'john@example.org',
'password': 'foo', 'password': 'foo',
@@ -299,6 +330,8 @@ def _authorization_step(client, ssoclient):
f'client_id={ssoclient[0].client_id}&' \ f'client_id={ssoclient[0].client_id}&' \
f'redirect_uri=https://example.net&' \ f'redirect_uri=https://example.net&' \
f'response_type=code&state=STATE&scope=openid+profile+email+phone' f'response_type=code&state=STATE&scope=openid+profile+email+phone'
if code_challenge:
url += f'&code_challenge={code_challenge}&code_challenge_method=S256'
r = client.get(url) r = client.get(url)
assert r.status_code == 302 assert r.status_code == 302
m = re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location']) m = re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location'])
@@ -373,6 +406,55 @@ def test_token_success(env, client, ssoclient):
CustomerSSOAccessToken.objects.get(token=d['access_token']).expires < now() CustomerSSOAccessToken.objects.get(token=d['access_token']).expires < now()
@pytest.mark.django_db
def test_token_pkce_required_if_used_in_authorization(env, client, ssoclient):
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
r = client.post('/bigevents/oauth2/v1/token', {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://example.net',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
assert r.status_code == 400
d = json.loads(r.content)
assert d['error'] == 'invalid_grant'
assert d['error_description'] == 'Missing of code_verifier'
@pytest.mark.django_db
def test_token_pkce_incorrect(env, client, ssoclient):
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
r = client.post('/bigevents/oauth2/v1/token', {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://example.net',
'code_verifier': "WRONG",
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
assert r.status_code == 400
d = json.loads(r.content)
assert d['error'] == 'invalid_grant'
assert d['error_description'] == 'Mismatch of code_verifier with code_challenge'
@pytest.mark.django_db
def test_token_success_pkce(env, client, ssoclient):
# this is the sample from the actual RFC
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
r = client.post('/bigevents/oauth2/v1/token', {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://example.net',
'code_verifier': code_verifier,
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
print(r.content)
assert r.status_code == 200
d = json.loads(r.content)
assert d['access_token']
@pytest.mark.django_db @pytest.mark.django_db
def test_scope_enforcement(env, client, ssoclient): def test_scope_enforcement(env, client, ssoclient):
ssoclient[0].allowed_scopes = ['openid', 'profile'] ssoclient[0].allowed_scopes = ['openid', 'profile']