Compare commits

...

67 Commits

Author SHA1 Message Date
Raphael Michel 4282613fd4 Add a log entry on manual VAT ID validation (Z#23223874) 2026-02-26 17:00:35 +01:00
Richard Schreiber a25bca7471 Fix static instance name in emails (Z#23224360) (#5914) 2026-02-25 13:19:53 +01:00
luelista da43984ad2 Add datasync logging (Z#23225588) (#5928)
* Fix inconsistent log messages

* Add logging for successfully synced orders

(debugging orders that might get silently skipped)
2026-02-25 09:49:52 +01:00
Martin Gross 7cce1c9219 PPv2: Handle paypal-payments/oders in 'created' status (Z#23225625) (#5929) 2026-02-25 09:21:58 +01:00
Martin Gross cb9c4466f9 Revert "PPv2: Do not put payments in pending-state if no capture has occured yet."
This reverts commit e5c8f19984.
2026-02-24 16:55:57 +01:00
Martin Gross 3398cda74b PPv2: properly check for pending-payments in pending-renderer 2026-02-24 16:16:22 +01:00
Martin Gross e5c8f19984 PPv2: Do not put payments in pending-state if no capture has occured yet. 2026-02-24 16:07:16 +01:00
Raphael Michel 5027f6dd59 Bump version to 2026.3.0.dev0 2026-02-24 13:37:15 +01:00
Raphael Michel 787db18d72 Bump version to 2026.2.0 2026-02-24 13:37:09 +01:00
Raphael Michel aadce7be00 Remove print statement from debugging (Z#23225586)
This was reported as a security issue, but we see no security impact or
exploitation path, as the security of PKCE relies on keeping the
verifier secret, not the challenge.
2026-02-24 13:36:52 +01:00
Raphael Michel 26f296bc11 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael Michel 6ae80cdd4b Translations: Update German
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael Michel cb3956c994 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-02-24 12:50:51 +01:00
Hijiri Umemoto b9f350bf3a Translations: Update Japanese
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-24 12:50:18 +01:00
Raphael Michel ab447bb85f Fix HTML injection in error message (Z#23225396) (#5921)
We're not treating it as a security issue as there is no vector to
inject the HTML into other people's browser, only one's own.
2026-02-24 12:48:43 +01:00
Raphael Michel bf33a42ae8 Validate request_id_header not to be misunderstood (Z#23225356) (#5920) 2026-02-24 12:48:25 +01:00
Lukas Bockstaller 081f975ff9 add missing slug fields (#5925) 2026-02-24 10:39:03 +01:00
Lukas Bockstaller eab7d81a51 Waiting list: Add edit view for entry (Z#23215496) (#5712)
* add edit view for waitinglist entry

* add test and fix behaviour when name isn't asked for

* fix linting

* add testcases for new edit view

* fix test

* fix linting

* add search to the waitinglist view

* repair settings check

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* make name and phone field optional by removing them

* remove item and variation fields from form

rather set those values during clean

* change label from "Item and Variation" to "Product"

* include only products with an enabled waitinglist in the product field

* combine edit.html and transfer.html

* change transfer to edit

* add tests

* code style

* Update src/pretix/control/forms/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/forms/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/urls.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/templates/pretixcontrol/waitinglist/edit.html

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/templates/pretixcontrol/waitinglist/index.html

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* remove validations

* remove validations

* replace widget

* implement small review items

* add better assertions

* add test for the different edit form variations

* add queryset to prefetch only active ItemVariations

* add queryset to prefetch only active ItemVariations

* propper use of WrappedPhoneNumberPrefixWidget

* cleanup

* add validation tests

* small review changes

* handle products with only inactive variations

* styling

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-02-23 16:35:24 +01:00
Hijiri Umemoto b2dce51a24 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-02-23 13:48:24 +01:00
Hijiri Umemoto 5bd660a913 Translations: Update Japanese
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-23 13:48:24 +01:00
Raphael Michel 8e9cdd7548 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Raphael Michel d6592cbb93 Translations: Update German
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Raphael Michel 0e3ccae5d4 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Raphael Michel 034b46d218 Translations: Update German
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Raphael Michel a3f120198d Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (6243 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
CVZ-es fa5f3bb15a Translations: Update Spanish
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
CVZ-es 5120b312b6 Translations: Update French
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Ruud Hendrickx 09064844b2 Translations: Update Dutch (Belgium)
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Ruud Hendrickx 1a60b3a712 Translations: Update Dutch (Belgium)
Currently translated at 26.8% (1677 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Ruud Hendrickx 6216f0d7df Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Ruud Hendrickx 380b55e699 Translations: Update Dutch
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-22 20:21:04 +01:00
Raphael Michel 6e67bb5045 Translations: Update wordlists 2026-02-22 20:18:46 +01:00
Raphael Michel 1463ee9227 Fix token message translation 2026-02-22 17:26:19 +01:00
Raphael Michel 3b49e77722 Login: Detect redirect loop and give users useful advice (#5911) 2026-02-22 16:59:14 +01:00
dependabot[bot] ceed07af94 Update isort requirement from ==7.0.* to ==8.0.* (#5910)
Updates the requirements on [isort](https://github.com/PyCQA/isort) to permit the latest version.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/7.0.0...8.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 8.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 16:59:06 +01:00
Raphael Michel 802c03f8f3 Mail: Fix stuck state when tickets are not available (Z#23225229) (#5917) 2026-02-22 16:58:41 +01:00
Martin Gross 9962d8a3be Stripe: |safe escape for action_redirect_url 2026-02-22 16:56:11 +01:00
Martin Gross 028a41f3e4 PPv2: Fix processing of purchase_units without payments 2026-02-20 16:50:34 +01:00
Richard Schreiber 6d8a9854f9 Update po files
[CI skip]

Signed-off-by: Richard Schreiber <schreiber@rami.io>
2026-02-20 14:01:40 +01:00
Richard Schreiber 861e14bb16 Update po files
[CI skip]

Signed-off-by: Richard Schreiber <schreiber@rami.io>
2026-02-20 13:53:54 +01:00
Richard Schreiber 7a080c0820 Fix typo and update wordlist for WERO 2026-02-20 13:52:53 +01:00
Richard Schreiber 2dbdb91066 Update po files
[CI skip]

Signed-off-by: Richard Schreiber <schreiber@rami.io>
2026-02-20 13:29:40 +01:00
Ruud Hendrickx b8efb8f61d Translations: Update Dutch (Belgium)
Currently translated at 17.1% (1067 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx 5f0cc4cc59 Translations: Update Albanian
Currently translated at 1.1% (71 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx d3bb1f3190 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx 69a215feff Translations: Update Dutch
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx 435dd5ebaf Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Mie Frydensbjerg 015d74f7ae Translations: Update Danish
Currently translated at 45.2% (2808 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx 5c9a069d77 Translations: Update Dutch (Belgium)
Currently translated at 9.7% (608 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx 5866cf94ee Translations: Update Dutch (Belgium)
Currently translated at 9.7% (606 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Mie Frydensbjerg fa15ba4435 Translations: Update Danish
Currently translated at 45.2% (2806 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx e982f04d59 Translations: Update Dutch (Belgium)
Currently translated at 5.1% (317 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Ruud Hendrickx ced00266dc Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-20 13:27:38 +01:00
Martin Gross b534c125db PPv2: Handle payment execution/capture calls properly even if no captures are present yet. (#5909) 2026-02-20 11:40:22 +01:00
Raphael Michel 769e1312d4 Revert "Disable partitioned cookies for Safari due to WebKit bugs (#5843)"
This reverts commit fbd8bbbeaa.
2026-02-20 10:08:51 +01:00
Martin Gross 3d53c03906 Stripe: isort 2026-02-19 14:43:27 +01:00
Martin Gross 59d1d2cb16 Stripe: Add Wero as a hidden payment method (private beta; requires MoR) 2026-02-19 14:40:01 +01:00
luelista 7e45837295 Security hardening for 2FA configuration (#5685)
* reduce default RecentAuthenticationRequiredMixin timeout to 15 min
* never cache pages with RecentAuthenticationRequiredMixin
* show emergency codes only once after generating
2026-02-19 12:43:23 +01:00
Lukas Bockstaller fd9ed15065 include acceptor slug in log/webhook event (#5906) 2026-02-19 10:00:11 +01:00
Richard Schreiber 2df3d9206b Add voucher tag to orderlist positions export 2026-02-19 09:42:00 +01:00
Kian Cross fbd8bbbeaa Disable partitioned cookies for Safari due to WebKit bugs (#5843)
Safari currently exhibits a bug where Partitioned cookies (CHIPS) are not
sent back to the originating site after multi-hop cross-site redirects,
breaking SSO login flows in pretix.

Partitioned cookies were initially introduced in Safari 18.4, removed
again in 18.5 due to a bug, and reintroduced in Safari 26.2, where the
current issue is present.

As a mitigation, disable sending the `Partitioned` attribute for Safari
user agents. This is intentionally conservative; once the Safari issue
is fixed, this check should be refined to be conditional on the affected
versions only.

WebKit issues:

  - https://bugs.webkit.org/show_bug.cgi?id=292975
  - https://bugs.webkit.org/show_bug.cgi?id=306194
2026-02-18 09:19:14 +01:00
Kara Engelhardt 1c305e4b30 Store failed offline checkin if successful online checkin with same nonce exists 2026-02-17 10:41:05 +01:00
KarlKeu00 ea114b4f64 Fix HTML closing tags in pending.html (#5893) 2026-02-17 10:20:28 +01:00
dependabot[bot] 0342613635 Update fakeredis requirement from ==2.33.* to ==2.34.* (#5899)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.33.0...v2.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 10:16:35 +01:00
dependabot[bot] 743c4b796b Update sentry-sdk requirement from ==2.52.* to ==2.53.* (#5898)
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.52.0a1...2.53.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 10:16:27 +01:00
Raphael Michel 8a7f54795e Vouchers: Fix field label inconsistency (Z#23222887) (#5902)
The field Voucher.price_mode is sometimes called "Price mode" and
sometimes "Price effect" in the UI, which is inconsistent. I think
"price effect" is a little clearer, but I don't really care as long as
it is consistent.
2026-02-17 10:16:12 +01:00
Raphael Michel cb464ad597 Remove back link from 404 error page (#23222967) (#5901)
I've kept it for 400/403/500/csrffail for now, because they also have a
"try again" link. Yes, both things have browser buttons, but they make
it a *little* clearer to technical users what one could to next, and
especially on csrffail, "step back" is always possible and possibly actually
helpful.
2026-02-17 10:16:05 +01:00
108 changed files with 148067 additions and 124883 deletions
+3 -3
View File
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.52.*",
"sentry-sdk==2.53.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -110,10 +110,10 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.33.*",
"fakeredis==2.34.*",
"flake8==7.3.*",
"freezegun",
"isort==7.0.*",
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=0.24",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.2.0.dev0"
__version__ = "2026.3.0.dev0"
+2 -1
View File
@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
+5 -1
View File
@@ -188,11 +188,15 @@ class CheckinListViewSet(viewsets.ModelViewSet):
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
prev = kwargs['position'].all_checkins.filter(
nonce=serializer.validated_data['nonce'],
successful=False
).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
successful=False
).first()
if prev:
# Ignore because nonce is already handled
+15 -3
View File
@@ -259,7 +259,14 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk, 'acceptor_id': self.request.organizer.id})
data=merge_dicts(
self.request.data,
{
'id': inst.pk,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
)
@transaction.atomic()
@@ -290,7 +297,11 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff, 'acceptor_id': self.request.organizer.id}
data={
'value': diff,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
return inst
@@ -320,7 +331,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
data={
'value': value,
'text': text,
'acceptor_id': self.request.organizer.id
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
+4 -1
View File
@@ -183,6 +183,7 @@ class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -197,7 +198,9 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -472,7 +475,7 @@ def register_default_webhook_events(sender, **kwargs):
),
ParametrizedGiftcardTransactionWebhookEvent(
'pretix.giftcards.transaction.*',
_('Gift card used in transcation'),
_('Gift card used in transaction'),
)
)
+5 -2
View File
@@ -216,7 +216,10 @@ class OutboundSyncProvider:
try:
mapped_objects = self.sync_order(sq.order)
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list]
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
if should_write_logentry:
sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier,
"objects": {
@@ -237,7 +240,7 @@ class OutboundSyncProvider:
sq.set_sync_error("exceeded", e.messages, e.full_message)
else:
logger.info(
f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"Could not sync order {sq.order.code} to {sq.sync_provider} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True,
)
+2
View File
@@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter):
pgettext('address', 'State'),
_('Voucher'),
_('Voucher budget usage'),
_('Voucher tag'),
_('Pseudonymization ID'),
_('Ticket secret'),
_('Seat ID'),
@@ -769,6 +770,7 @@ class OrderListExporter(MultiSheetListExporter):
op.state_for_address or '',
op.voucher.code if op.voucher else '',
op.voucher_budget_use if op.voucher_budget_use else '',
op.voucher.tag if op.voucher else '',
op.pseudonymization_id,
op.secret,
]
+3 -3
View File
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price mode')
verbose_name = gettext_lazy('Price effect')
default_value = None
initial = 'static:none'
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
elif value in reverse:
return reverse[value]
else:
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
value=value, options=', '.join(d.keys())
))
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
def clean(self, value, previous_values):
value = super().clean(value, previous_values)
if value and previous_values.get("price_mode") == "none":
raise ValidationError(_("It is pointless to set a value without a price mode."))
raise ValidationError(_("It is pointless to set a value without a price effect."))
return value
def assign(self, value, obj: Voucher, **kwargs):
+4 -1
View File
@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
+1 -1
View File
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
def set_sync_error(self, failure_mode, messages, full_message):
logger.exception(
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})"
)
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider,
+1 -1
View File
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
)
)
price_mode = models.CharField(
verbose_name=_("Price mode"),
verbose_name=_("Price effect"),
max_length=100,
choices=PRICE_MODES,
default='none'
+3 -1
View File
@@ -1650,7 +1650,8 @@ class GiftCardPayment(BasePaymentProvider):
action='pretix.giftcards.transaction.payment',
data={
'value': trans.value,
'acceptor_id': self.event.organizer.id
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug
}
)
except PaymentException as e:
@@ -1682,6 +1683,7 @@ class GiftCardPayment(BasePaymentProvider):
data={
'value': refund.amount,
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug,
'text': refund.comment,
}
)
+13 -4
View File
@@ -389,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
# mail_send_task(self, *, outgoing_mail)
with scopes_disabled():
mail_send(**kwargs)
return
return False
else:
raise ValueError("Unknown arguments")
@@ -443,15 +443,24 @@ def mail_send_task(self, **kwargs) -> bool:
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)
except Exception:
except Exception as e:
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
# why (probably some race condition with ticket cache invalidation?), so retry later.
try:
self.retry(max_retries=5, countdown=60)
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
retry_after = 60
outgoing_mail.error = "Tickets not ready"
outgoing_mail.error_detail = str(e)
outgoing_mail.sent = now()
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
"actual_attachments"])
self.retry(max_retries=5, countdown=retry_after)
except MaxRetriesExceededError:
# Well then, something is really wrong, let's send it without attachment before we
# don't send at all
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
pass
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
+8 -3
View File
@@ -253,7 +253,8 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
auth=auth,
data={
'value': position.price,
'acceptor_id': order.event.organizer.id
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
break
@@ -563,6 +564,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
data={
'value': -position.price,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
@@ -2457,7 +2459,8 @@ class OrderChangeManager:
auth=self.auth,
data={
'value': -position.price,
'acceptor_id': self.order.event.organizer.id
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
}
)
@@ -2483,7 +2486,8 @@ class OrderChangeManager:
auth=self.auth,
data={
'value': -opa.position.price,
'acceptor_id': self.order.event.organizer.id
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
}
)
@@ -3453,6 +3457,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
data={
'value': trans.value,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
any_giftcards = True
+1
View File
@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
-3
View File
@@ -8,9 +8,6 @@
<h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p>
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
+89 -1
View File
@@ -19,17 +19,44 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
from phonenumber_field.formfields import PhoneNumberField
from pretix.base.forms import I18nModelForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
)
from pretix.base.models import WaitingListEntry
from pretix.control.forms.widgets import Select2
class WaitingListEntryTransferForm(I18nModelForm):
class WaitingListEntryEditForm(I18nModelForm):
itemvar = forms.ChoiceField(
error_messages={
'invalid_choice': _("Select a valid choice.")
}
)
def __init__(self, *args, **kwargs):
self.instance = kwargs.get('instance', None)
initial = kwargs.get('initial', {})
choices = []
if self.instance and self.instance.pk and 'itemvar' not in initial:
if self.instance.variation is not None:
initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}'
if self.instance.variation.active is False:
choices.append((initial['itemvar'], str(self.instance.variation)))
else:
initial['itemvar'] = self.instance.item.pk
if self.instance.item.active is False:
choices.append((initial['itemvar'], str(self.instance)))
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if self.event.has_subevents:
@@ -45,12 +72,73 @@ class WaitingListEntryTransferForm(I18nModelForm):
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
else:
del self.fields['subevent']
if self.event.settings.waiting_list_names_asked:
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=self.event.settings.waiting_list_names_required,
scheme=self.event.organizer.settings.name_scheme,
titles=self.event.organizer.settings.name_scheme_titles,
label=_('Name'),
)
else:
del self.fields['name_parts']
if not self.event.settings.waiting_list_phones_asked:
del self.fields['phone']
items = self.event.items.filter(active=True).prefetch_related(
'variations'
)
for item in items:
if len(item.variations.all()) > 0:
for variation in item.variations.all():
if variation.active:
choices.append(
('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation)))
)
else:
choices.append(('{}'.format(item.pk), str(item)))
self.fields['itemvar'].label = _("Product")
self.fields['itemvar'].help_text = _("Only includes active products.")
self.fields['itemvar'].required = True
self.fields['itemvar'].choices = choices
def clean(self):
cleaned_data = super().clean()
if self.instance.voucher is not None:
raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.'))
itemvar = cleaned_data.get('itemvar')
if itemvar:
self.instance.item = self.event.items.get(pk=itemvar.split('-')[0])
if '-' in itemvar:
self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1])
if ((self.instance.item and not self.instance.item.active) or
(self.instance.variation and not self.instance.variation.active)):
self.add_error('itemvar', _('The selected product is not active.'))
return cleaned_data
class Meta:
model = WaitingListEntry
fields = [
'email',
'name_parts',
'phone',
'subevent',
]
field_classes = {
'subevent': SafeModelChoiceField,
'email': forms.EmailField,
'phone': PhoneNumberField,
}
widgets = {
'phone': WrappedPhoneNumberPrefixWidget,
}
+1
View File
@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
@@ -19,6 +19,14 @@
</ul>
<br>
{% endif %}
{% if possible_cookie_problem %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please
check if your browser is set to block cookies, or delete all existing cookies and retry.
{% endblocktrans %}
</div>
{% endif %}
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group buttons">
@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your pretix team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on pretix, a platform to perform event
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
Organizer: {{ organizer }}
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
@@ -12,5 +12,5 @@ You can review and change your account settings here:
{{ url }}
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -144,14 +144,23 @@
</div>
<div class="panel-body">
<p>
{% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
{% blocktrans trimmed %}
If you lose access to your devices, you can use one of your emergency tokens to log in.
We recommend to store them in a safe place, e.g. printed out or in a password manager.
Every token can be used at most once.
{% endblocktrans %}
</p>
<p>{% trans "Unused tokens:" %}</p>
<ul>
{% for t in static_tokens %}
<li><code>{{ t.token }}</code></li>
{% endfor %}
</ul>
{% if static_tokens_device %}
<p>
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% endif %}
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %}
@@ -0,0 +1,33 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Edit entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Edit entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
{% bootstrap_field form.email layout="control" %}
{% if form.name_parts %}
{% bootstrap_field form.name_parts layout="control" %}
{% endif %}
{% if form.phone %}
{% bootstrap_field form.phone layout="control" %}
{% endif %}
{% bootstrap_field form.itemvar layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -124,6 +124,7 @@
</option>
{% endfor %}
</select>
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
@@ -267,13 +268,13 @@
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
{% if request.event.has_subevents %}
<a href="{% url "control:event.orders.waitinglist.transfer" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Transfer to other date" context "subevent" %}"
data-toggle="tooltip">
<i class="fa fa-calendar" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %}
<button class="btn btn-default btn-sm disabled">
@@ -1,23 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Transfer entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Transfer entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans trimmed context "subevent" %}
Please select the date to which the following waiting list entry should be
transferred: <strong>{{ entry }}</strong>?
{% endblocktrans %}</p>
{% bootstrap_field form.subevent layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Transfer" %}
</button>
</div>
</form>
{% endblock %}
+2 -2
View File
@@ -480,8 +480,8 @@ urlpatterns = [
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'),
re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
name='event.orders.waitinglist.transfer'),
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
name='event.orders.waitinglist.edit'),
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
+2
View File
@@ -149,6 +149,8 @@ def login(request):
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
else:
form = LoginForm(backend=backend, request=request)
# Detect redirection loop (usually means cookie not accepted)
ctx['possible_cookie_problem'] = request.path in request.headers.get("Referer", "")
ctx['form'] = form
ctx['can_register'] = settings.PRETIX_REGISTRATION
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
+8 -4
View File
@@ -870,11 +870,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
PlaceholderValidator.error_message
)
except KeyError as e:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
)
return JsonResponse({
'item': preview_item,
+11 -3
View File
@@ -1641,9 +1641,17 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:
+6 -3
View File
@@ -1039,9 +1039,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
@@ -1850,7 +1851,8 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
data={
'value': value,
'text': request.POST.get('text'),
'acceptor_id': self.request.organizer.id
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
},
user=self.request.user,
)
@@ -1913,7 +1915,8 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
user=self.request.user,
data={
'value': form.cleaned_data['value'],
'acceptor_id': self.request.organizer.id
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
return redirect(reverse(
+16 -10
View File
@@ -49,12 +49,14 @@ from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -85,8 +87,9 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin:
max_time = 3600
max_time = 900
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
@@ -289,16 +292,13 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
ctx = super().get_context_data()
try:
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency')
except StaticDevice.MultipleObjectsReturned:
ctx['static_tokens'] = StaticDevice.objects.filter(
ctx['static_tokens_device'] = StaticDevice.objects.filter(
user=self.request.user, name='emergency'
).first().token_set.all()
).first()
except StaticDevice.DoesNotExist:
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
for i in range(10):
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
ctx['static_tokens'] = d.token_set.all()
ctx['static_tokens_device'] = None
ctx['devices'] = []
for dt in REAL_DEVICE_TYPES:
@@ -630,8 +630,14 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
])
self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user)
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices.'))
messages.success(
request,
_('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices. You will not be able to view them '
'again here.\n\nYour emergency codes:\n{tokens}').format(
tokens='- ' + '\n- '.join(t.token for t in d.token_set.all())
)
)
return redirect(reverse('control:user.settings.2fa'))
+17 -11
View File
@@ -53,7 +53,7 @@ from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import assign_automatically
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.waitinglist import WaitingListEntryTransferForm
from pretix.control.forms.waitinglist import WaitingListEntryEditForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin
@@ -138,6 +138,17 @@ class WaitingListQuerySetMixin:
elif force_filtered and '__ALL' not in self.request_data:
qs = qs.none()
if self.request_data.get("search", "") != "":
s = self.request_data.get("search", "")
search_q = Q(email__icontains=s)
if self.request.event.settings.waiting_list_names_asked:
search_q = search_q | Q(name_cached__icontains=s)
if self.request.event.settings.waiting_list_phones_asked:
search_q = search_q | Q(phone__icontains=s)
qs = qs.filter(search_q)
return qs
@@ -238,7 +249,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = Item.objects.filter(event=self.request.event)
ctx['filtered'] = ("status" in self.request.GET or "item" in self.request.GET)
ctx['filtered'] = any(param in self.request.GET for param in ("status", "item", "search"))
itemvar_cache = {}
quota_cache = {}
@@ -390,25 +401,20 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
})
class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/transfer.html'
template_name = 'pretixcontrol/waitinglist/edit.html'
permission = 'can_change_orders'
form_class = WaitingListEntryTransferForm
form_class = WaitingListEntryEditForm
context_object_name = 'entry'
def dispatch(self, request, *args, **kwargs):
if not self.request.event.has_subevents:
raise Http404(_("This is not an event series."))
return super().dispatch(request, *args, **kwargs)
def get_object(self, queryset=None) -> WaitingListEntry:
return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True)
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('The waitinglist entry has been transferred.'))
if form.has_changed():
messages.success(self.request, _('The waitinglist entry has been changed.'))
self.object.log_action(
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -265,6 +265,7 @@ Objekt-IDs
Offline-Scan
OK
Online-Banking
Online-Banking-Nutzer
Onlinebanking
Onlinebanking-Zugangsdaten
Open
@@ -539,6 +540,8 @@ WeChat-Zahlung
Weiterleitungs-URIs
Weiterleitungs-URL
Weiterleitungs-URLs
WERO
WERO-App
WhatsApp
Widget
Widget-Code
File diff suppressed because it is too large Load Diff
@@ -265,6 +265,7 @@ Objekt-IDs
Offline-Scan
OK
Online-Banking
Online-Banking-Nutzer
Onlinebanking
Onlinebanking-Zugangsdaten
Open
@@ -539,6 +540,8 @@ WeChat-Zahlung
Weiterleitungs-URIs
Weiterleitungs-URL
Weiterleitungs-URLs
WERO
WERO-App
WhatsApp
Widget
Widget-Code
+2561 -2267
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:20+0000\n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.2\n"
"X-Generator: Weblate 5.16\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -256,7 +256,7 @@ msgstr "承認保留中"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
msgstr "使用済"
msgstr "引き換え済み"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
"PO-Revision-Date: 2026-02-19 22:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.2\n"
"X-Generator: Weblate 5.16\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -674,7 +674,7 @@ msgstr "Zoekopdracht"
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Alle"
msgstr "Allemaal"
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -187,6 +187,7 @@ webhooks
webserver
Wechat
WeChat
WERO
WhatsApp
whitespace
xlsx
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -21,10 +21,10 @@
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
{% if settings.bank_details_type == "sepa" %}
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd>
{% endif %}
</dl>
{% if details %}
@@ -38,4 +38,4 @@
{% if payment_qr_codes %}
{% include "pretixpresale/event/payment_qr_codes.html" %}
{% endif %}
</div>
</div>
+39 -26
View File
@@ -786,7 +786,12 @@ class PaypalMethod(BasePaymentProvider):
else:
pp_captured_order = response.result
for purchaseunit in pp_captured_order.purchase_units:
payment.refresh_from_db()
any_captures = False
all_captures_completed = True
for purchaseunit in pp_captured_order.purchase_units:
if hasattr(purchaseunit, 'payments'):
for capture in purchaseunit.payments.captures:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
@@ -794,32 +799,40 @@ class PaypalMethod(BasePaymentProvider):
pass
if capture.status != 'COMPLETED':
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment.info = json.dumps(pp_captured_order.dict())
payment.state = OrderPayment.PAYMENT_STATE_PENDING
payment.save()
return
all_captures_completed = False
else:
any_captures = True
payment.refresh_from_db()
if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict())
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
raise PaymentException(
_('We were unable to process your payment. See below for details on how to proceed.')
)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('PayPal success event even though order is already marked as paid')
return
try:
# Payment has at least one capture, but it is not yet completed
if any_captures and not all_captures_completed:
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
payment.state = OrderPayment.PAYMENT_STATE_PENDING
payment.save()
return
# Payment has at least one capture and all captures are completed
elif any_captures and all_captures_completed:
if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict())
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
raise PaymentException(
_('We were unable to process your payment. See below for details on how to proceed.')
)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('PayPal success event even though order is already marked as paid')
return
try:
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
# Payment has not any captures yet - so it's probably in created status
else:
return
finally:
if 'payment_paypal_oid' in request.session:
del request.session['payment_paypal_oid']
@@ -829,7 +842,7 @@ class PaypalMethod(BasePaymentProvider):
try:
if (
payment.info
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'pending'
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'PENDING'
):
retry = False
except (KeyError, IndexError):
+22
View File
@@ -118,6 +118,7 @@ logger = logging.getLogger('pretix.plugins.stripe')
# - UPI: ✗
# - Netbanking: ✗
# - TWINT: ✓
# - Wero: ✓ (No settings UI yet)
#
# Bank transfers
# - ACH Bank Transfer: ✗
@@ -509,6 +510,15 @@ class StripeSettingsHolder(BasePaymentProvider):
'before they work properly.'),
required=False,
)),
# Disabled for now, since still in closed Beta and only available to dedicated boarded accounts.
# ('method_wero',
# forms.BooleanField(
# label=_('Wero'),
# disabled=self.event.currency not in 'EUR',
# help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account '
# 'before they work properly.'),
# required=False,
# )),
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
)
if not self.settings.connect_client_id or self.settings.secret_key:
@@ -1946,3 +1956,15 @@ class StripeMobilePay(StripeRedirectMethod):
"type": "mobilepay",
},
}
class StripeWero(StripeRedirectMethod):
identifier = 'stripe_wero'
verbose_name = _('WERO via Stripe')
public_name = 'WERO'
method = 'wero'
confirmation_method = 'automatic'
explanation = _(
'This payment method is available to European online banking users, whose banking institutions support WERO '
'either through their native banking apps or through the WERO wallet app. Please have you app ready.'
)

Some files were not shown because too many files have changed in this diff Show More