Compare commits

...
Author SHA1 Message Date
Richard Schreiber 6d8d06e78f fix isort 2024-04-15 16:26:11 +02:00
Richard Schreiber 3bde9d8b0e Fix: show country name in countrycodes question in profile description 2024-04-15 16:26:11 +02:00
Richard Schreiber 11956a8f4d fix isort 2024-04-15 16:25:25 +02:00
Raphael Michel d0c58713c4 Badge/ticket export: Allow to sort by company 2024-04-15 10:55:40 +02:00
Raphael Michel 46da0bda61 Placeholders: Use dynamic sample for name_for_salutation 2024-04-15 10:55:40 +02:00
Raphael MichelandGitHub 009e3a6d36 Respect Order.valid_if_pending for mail_attach_ical_paid_only (#4065) 2024-04-12 15:00:00 +02:00
Martin GrossandGitHub c01855270b Approval Process: Attach ical to approval-messages if properly configured (#4064) 2024-04-12 11:18:21 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2df1585b71 Update sentry-sdk requirement from ==1.44.* to ==1.45.* (#4058)
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/1.44.0...1.45.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>
2024-04-11 13:33:01 +02:00
Raphael Michel bf48ae567f NREI export: Fix parsing of stripe data 2024-04-11 09:27:55 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5a72c72d18 Update django-otp requirement from ==1.3.* to ==1.4.* (#4057)
Updates the requirements on [django-otp](https://github.com/django-otp/django-otp) to permit the latest version.
- [Changelog](https://github.com/django-otp/django-otp/blob/master/CHANGES.rst)
- [Commits](https://github.com/django-otp/django-otp/compare/v1.3.0...v1.4.0)

---
updated-dependencies:
- dependency-name: django-otp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 09:56:40 +02:00
Raphael Michel ac02f3b417 API: Fix crash in order creation (PRETIXEU-9XS) 2024-04-10 09:40:35 +02:00
Martin GrossandGitHub 58add74b3a Stripe: Add PayPal (#4049) (Z#23123667) 2024-04-09 10:06:58 +02:00
Raphael MichelandGitHub 0067c3537d Fix invalid orders being created in a complex situation (#4054)
This was a bug that took days to find. The story goes like this: A cart
is created with four positions that each include four bundled positions.
A discount is applied, changing the price of *one* of the four top-level
positions to a reduced value. The list of position IDs gets passed to
`perform_order()`, which later passes it on to `transform_cart_positions()`.
`transform_cart_positions()`, however, receives the positions in an order
that has the first-level product *after* the bundled products that
belong to it. Therefore, it can't properly assign the parent-child
relationship between the positions.

The main reason is that cart positions are processed in "database order"
in a number of places, i.e. we make `SELECT` queries without an explicit
`ORDER BY` statement, leading the database to respond in unspecified
order. This is the case for `get_cart()` and hence for `CartMixin.positions`,
and hence for the list of position IDs that is passed to `perform_order()`
and hence for the order in which discounts are processed.

Therefore, if this "databse order" of the cart positions changes, the
discount compuation in `_check_positions()` might make a different choice
of *which* cart position should receive the discount than the CartManager
originally did. That's not nice, but most customers would not even
notice that a different one of their four (otherwise identical) tickets
is now discounted than the cart originally showed.

This leads to `_check_positions()` changing the price on two of the
cart positions. However, it only changes the price on the copy of
the CartPosition object that is directly part of the positions array,
while the `addon_to` attribute of its bundled positions contain a
*different* representation of the same cart position, that is not
refreshed to have the updated price now in the database.

This causes the `CartPosition.sort_key` of the bundled products to be
significantly different from the one of their parent products, which can
cause `transform_cart_positions()` to try to insert them before their
respective parent product, which is how the bug leads to the nasty end
result.

Now, I'm still not sure why this has happened *now* for the first time,
but I suspect it *might* even have something to do with our operations
team tuning our autovacuum parameters on our production installation,
which might make it *more likely* that newly created cart positions are
arbitrarily  stored on PostgreSQL disk pages in a different order than
they were inserted than before.

This commit now fixes the bug now in two ways, each of which would be
sufficient to fix it for now, but together they make it hopefully more
stable in the future:

- `perform_order` no longer respects the order of the position IDs it
  gets passed in, but instead uses the order last displayed in the cart.
  Additionally, both `CartManager` and `_check_positions()` now sort
  positions by their `pk` value before applying discounts to ensure
  consistent choice of which position is discounted (using  `sort_key`
  here does not make much sense since it includes sorting by price,
  which is about to change).

- `_check_positions()` makes sure that after its completion, only one
  copy of the same `CartPosition` is in use that has the current price.

Additionally, this commit makes sure `sort_key` cache is cleared after
e.g. a price change.

It was hard to write a regression test, since "database order" is, by
definition, unreliable, but I tried my best.
2024-04-08 16:55:54 +02:00
Raphael Michel 64ae1d08a6 Docs: Fix incorrect API response 2024-04-05 17:28:44 +02:00
Raphael Michel ca25c3c81e Add logging for special bug case (Z#23149646) 2024-04-04 18:13:54 +02:00
Raphael Michel abbe9ec897 Order creation: Fail loudly on invalid addon-to relationship 2024-04-03 17:21:47 +02:00
Raphael MichelandGitHub a7735d5d9e API: Allow request_valid_from in the past (#4048) 2024-04-03 17:21:25 +02:00
Raphael MichelandRaphael Michel 174c81a22b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5598 of 5598 strings)

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

powered by weblate
2024-04-03 13:15:57 +02:00
Raphael MichelandRaphael Michel 38c6294ede Translations: Update German
Currently translated at 100.0% (5598 of 5598 strings)

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

powered by weblate
2024-04-03 13:15:57 +02:00
Raphael Michel 217ae90642 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-04-03 12:43:52 +02:00
Raphael Michel 0c998ca884 Remove special handling of translations in pretix/helpers 2024-04-03 12:43:20 +02:00
Raphael MichelandRaphael Michel b1691f867d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5594 of 5594 strings)

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

powered by weblate
2024-04-03 11:43:39 +02:00
Raphael MichelandRaphael Michel 72e451b27b Translations: Update German
Currently translated at 100.0% (5594 of 5594 strings)

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

powered by weblate
2024-04-03 11:43:39 +02:00
Raphael Michel 8124ced6c1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-04-03 11:22:25 +02:00
Raphael MichelandGitHub a3139944f6 Send notifications about login with new client or country (#4032)
* Send notifications about login with new client or country

* Rebase migration

* Remove immediately

* Fix isort

* Text update
2024-04-03 11:19:20 +02:00
Felix SchäferandGitHub 48493c517b Add database.disable_server_side_cursors option (#4016) 2024-04-03 10:16:48 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
535a29bf4b Update css-inline requirement from ==0.13.* to ==0.14.* (#4043)
Updates the requirements on [css-inline](https://github.com/Stranger6667/css-inline) to permit the latest version.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/c-v0.13.0...c-v0.14.0)

---
updated-dependencies:
- dependency-name: css-inline
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 10:16:13 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9d415f5179 Update pillow requirement from ==10.2.* to ==10.3.* (#4044)
Updates the requirements on [pillow](https://github.com/python-pillow/Pillow) to permit the latest version.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 10:16:03 +02:00
990e9da21d Generalize import process from orders to more models (#4002)
* Generalize import process from orders to more models

* Add voucher import

* Model import: Guess assignments of based on column headers

* Fix lock_seats being pointless

* Update docs

* Update doc/development/api/import.rst

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

* Update src/pretix/base/modelimport_vouchers.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-04-03 10:15:30 +02:00
4afb7a4976 Allow admins to generate emergency 2FA tokens (#4035)
* Allow admins to generate emergency 2FA tokens

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

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-04-03 10:15:17 +02:00
Raphael MichelandRaphael Michel 22e5579ed1 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5585 of 5585 strings)

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

powered by weblate
2024-04-03 10:14:56 +02:00
Raphael MichelandRaphael Michel 79cd84e243 Translations: Update German
Currently translated at 100.0% (5585 of 5585 strings)

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

powered by weblate
2024-04-03 10:14:56 +02:00
Raphael MichelandRaphael Michel fad4b8846c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5585 of 5585 strings)

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

powered by weblate
2024-04-03 10:14:56 +02:00
Raphael MichelandRaphael Michel 0f4790afd8 Translations: Update German
Currently translated at 100.0% (5585 of 5585 strings)

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

powered by weblate
2024-04-03 10:14:56 +02:00
Raphael Michel 2068a5ac29 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-04-02 17:53:26 +02:00
Raphael MichelandGitHub 440c97061c Fix duplicate key on SQLite (#4038)
* Fix duplicate key on SQLite

* Update migration
2024-04-02 17:37:37 +02:00
Raphael MichelandRaphael Michel 3b6d0c4341 Translations: Add Slovak 2024-04-02 17:28:11 +02:00
Raphael MichelandRaphael Michel 06ac4b0250 Translations: Add Slovak 2024-04-02 17:28:11 +02:00
Raphael MichelandGitHub a233b92f6f Add disable date of waiting list to event timeline (#4036) 2024-04-02 17:15:41 +02:00
Raphael MichelandGitHub 4ea4189e6d Allow team admins to require two-factor authentication (#4034)
* Allow team admins to require two-factor authentication

* Add API tests

* Improve logic

* ADd button tooltip
2024-04-02 17:15:16 +02:00
MiraandGitHub 50838b9cea Update github action versions (#4033) 2024-04-02 13:53:09 +02:00
Raphael MichelandGitHub c68ee56d51 Log discarding a valid session for suspicious reasons (#4025) 2024-04-02 13:52:30 +02:00
ThatthepandRaphael Michel 5c0587c30e Translations: Update Thai
Currently translated at 0.6% (36 of 5574 strings)

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

powered by weblate
2024-04-02 11:45:11 +02:00
Raphael Michel f3f42a8a42 Login: Add logging for incorrect JS hostnames 2024-04-02 11:34:43 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20d0a9a0ed Update django-countries requirement from ==7.5.* to ==7.6.* (#4031)
Updates the requirements on [django-countries](https://github.com/SmileyChris/django-countries) to permit the latest version.
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.rst)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v7.5...v7.6.1)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 11:07:51 +02:00
cda8144ff0 Enforce uniqueness of order codes and ticket secrets (#3988)
* Enforce uniqueness of order codes and ticket secrets

* Fix test cases which created orders with identical codes

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-04-02 11:07:40 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
43e8875c1e Update sentry-sdk requirement from ==1.42.* to ==1.44.* (#4021)
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/1.42.0...1.44.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>
2024-04-02 10:26:37 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
28c142b2ed Update pytest-mock requirement from ==3.12.* to ==3.14.* (#4009)
Updates the requirements on [pytest-mock](https://github.com/pytest-dev/pytest-mock) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.12.0...v3.14.0)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:14:24 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
46203fd8ba Bump @babel/core from 7.24.0 to 7.24.3 in /src/pretix/static/npm_dir (#4027)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.0 to 7.24.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.3/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:13:39 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
52e45c37df Update webauthn requirement from ==2.0.* to ==2.1.* (#4022)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:13:17 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d1580dca2c Bump django-filter from 24.1 to 24.2 (#4017)
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 24.1 to 24.2.
- [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.1...24.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:13:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cd9e672871 Bump pycparser from 2.21 to 2.22 (#4029)
Bumps [pycparser](https://github.com/eliben/pycparser) from 2.21 to 2.22.
- [Release notes](https://github.com/eliben/pycparser/releases)
- [Changelog](https://github.com/eliben/pycparser/blob/main/CHANGES)
- [Commits](https://github.com/eliben/pycparser/compare/release_v2.21...release_v2.22)

---
updated-dependencies:
- dependency-name: pycparser
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:10:42 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
427f508627 Bump @babel/preset-env from 7.24.0 to 7.24.3 in /src/pretix/static/npm_dir (#4028)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.0 to 7.24.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.3/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 10:10:01 +02:00
Raphael Michel 887d06a485 Add .watchmanconfig 2024-03-31 23:14:57 +02:00
fb49046ac1 Log and count user logins (#4020)
* Log and count user logins

* Allow metrics without label

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-03-28 17:18:51 +01:00
Martin Gross ce826e50f7 PDF: Check for allowed font name before adding modifiers to the name 2024-03-27 12:54:35 +01:00
Raphael Michel d866c6954d Bump to 2024.4.0.dev0 2024-03-27 12:15:31 +01:00
177 changed files with 78304 additions and 36681 deletions
+3 -3
View File
@@ -26,12 +26,12 @@ jobs:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+3 -3
View File
@@ -25,12 +25,12 @@ jobs:
name: Spellcheck
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+6 -6
View File
@@ -23,12 +23,12 @@ jobs:
runs-on: ubuntu-22.04
name: Check gettext syntax
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -48,12 +48,12 @@ jobs:
runs-on: ubuntu-22.04
name: Spellcheck
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+8 -8
View File
@@ -23,12 +23,12 @@ jobs:
name: isort
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -43,12 +43,12 @@ jobs:
name: flake8
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -63,9 +63,9 @@ jobs:
name: licenseheaders
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install Dependencies
+3 -3
View File
@@ -32,7 +32,7 @@ jobs:
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '15'
@@ -41,10 +41,10 @@ jobs:
postgresql password: 'postgres'
if: matrix.database == 'postgres'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+9 -2
View File
@@ -97,8 +97,9 @@ Example::
Defaults to ``off``.
``obligatory_2fa``
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
Defaults to ``False``
Enables or disables obligatory usage of two-factor authentication for users of the pretix backend.
Can be ``True`` to make two-factor authentication obligatory for all users or ``staff`` to make it only
obligatory to users with admin permissions. Defaults to ``False``.
``trust_x_forwarded_for``
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
@@ -157,6 +158,7 @@ Example::
host=localhost
port=3306
advisory_lock_index=1
disable_server_side_cursors=0
sslmode=require
sslrootcert=/etc/pretix/postgresql-ca.crt
sslcert=/etc/pretix/postgresql-client-crt.crt
@@ -177,6 +179,11 @@ Example::
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
you should set separate values for this setting (integers up to 256).
``disable_server_side_cursors``
On PostgreSQL pretix might use server side cursors for certain operations. This is generally fine but will break in
specific circumstances, for example when connecting to PostgreSQL through a PGBouncer configured with a transaction
pool mode. Off by default (i.e. by default server side cursors will be used).
``sslmode``, ``sslrootcert``
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
+4 -1
View File
@@ -249,7 +249,10 @@ You can get three response codes:
Content-Type: application/json
{
"event": "democon",
"event": {
"name": "Demo Conference",
"slug": "democon"
},
"subevent": 23,
"checkinlist": 5
}
+7
View File
@@ -22,6 +22,8 @@ id integer Internal ID of
name string Team name
all_events boolean Whether this team has access to all events
limit_events list List of event slugs this team has access to
require_2fa boolean Whether members of this team are required to use
two-factor authentication
can_create_events boolean
can_change_teams boolean
can_change_organizer_settings boolean
@@ -122,6 +124,7 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -159,6 +162,7 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -186,6 +190,7 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -203,6 +208,7 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -246,6 +252,7 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
+13 -6
View File
@@ -3,11 +3,12 @@
.. _`importcol`:
Extending the order import process
==================================
Extending the import process
============================
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
It's possible through the backend to import objects into pretix, for example orders from a legacy ticketing system. If
your plugin defines additional data structures around those objects, it might be useful to make it possible to import
them as well.
Import process
--------------
@@ -40,7 +41,7 @@ Column registration
The import API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available import columns. Your plugin
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
should listen for this signal and return the subclass of ``pretix.base.modelimport.ImportColumn``
that we'll provide in this plugin:
.. sourcecode:: python
@@ -56,10 +57,16 @@ that we'll provide in this plugin:
EmailColumn(sender),
]
Similar signals exist for other objects:
.. automodule:: pretix.base.signals
:members: voucher_import_columns
The column class API
--------------------
.. class:: pretix.base.orderimport.ImportColumn
.. class:: pretix.base.modelimport.ImportColumn
The central object of each import extension is the subclass of ``ImportColumn``.
+10 -9
View File
@@ -33,14 +33,14 @@ dependencies = [
"celery==5.3.*",
"chardet==5.2.*",
"cryptography>=3.4.2",
"css-inline==0.13.*",
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==4.2.*",
"django-bootstrap3==23.6.*",
"django-compressor==4.4",
"django-countries==7.5.*",
"django-filter==24.1",
"django-countries==7.6.*",
"django-filter==24.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==1.1.*",
@@ -50,7 +50,7 @@ dependencies = [
"django-localflavor==4.0",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.3.*",
"django-otp==1.4.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
@@ -75,12 +75,12 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.8.*",
"phonenumberslite==8.13.*",
"Pillow==10.2.*",
"Pillow==10.3.*",
"pretix-plugin-build",
"protobuf==5.26.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
@@ -92,7 +92,7 @@ dependencies = [
"redis==5.0.*",
"reportlab==4.1.*",
"requests==2.31.*",
"sentry-sdk==1.42.*",
"sentry-sdk==1.45.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
@@ -100,9 +100,10 @@ dependencies = [
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==0.18.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.0.*",
"webauthn==2.1.*",
"zeep==4.2.*"
]
@@ -122,7 +123,7 @@ dev = [
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.12.*",
"pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar",
"pytest-xdist==3.5.*",
+4
View File
@@ -0,0 +1,4 @@
{
"ignore_dirs": ["node_modules", "data", "pretix/static", "pretix/locale", "pretix/static.dist"]
}
+1 -1
View File
@@ -6,7 +6,7 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n
+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__ = "2024.3.0"
__version__ = "2024.4.0.dev0"
+14 -1
View File
@@ -39,7 +39,8 @@ from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
SessionReauthRequired, assert_session_valid,
)
@@ -66,6 +67,10 @@ class EventPermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
else request.user)
@@ -144,6 +149,10 @@ class ProfilePermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
if isinstance(request.auth, OAuthAccessToken):
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
@@ -166,5 +175,9 @@ class AnyAuthenticatedClientPermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
return True
+3 -2
View File
@@ -1315,7 +1315,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
valid_from, valid_until = pos_data['item'].compute_validity(
requested_start=(
max(requested_valid_from, now())
requested_valid_from
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
else now()
),
@@ -1439,6 +1439,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
else:
pos._calculate_tax(invoice_address=ia)
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = listed_price
@@ -1466,7 +1467,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax()
pos._calculate_tax(invoice_address=ia)
if simulate:
pos = WrappedModel(pos)
+1 -1
View File
@@ -239,7 +239,7 @@ class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = (
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
+9 -2
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import datetime
import logging
import mimetypes
import os
from decimal import Decimal
@@ -27,7 +28,7 @@ from zoneinfo import ZoneInfo
import django_filters
from django.conf import settings
from django.db import transaction
from django.db import IntegrityError, transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
)
@@ -97,6 +98,8 @@ from pretix.base.signals import (
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
logger = logging.getLogger(__name__)
with scopes_disabled():
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
@@ -900,7 +903,11 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
def perform_create(self, serializer):
serializer.save()
try:
serializer.save()
except IntegrityError:
logger.exception("Integrity error while saving order")
raise ValidationError("Integrity error, possibly duplicate submission of same order.")
def perform_destroy(self, instance):
if not instance.testmode:
+1 -1
View File
@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, currencies, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings
+18 -4
View File
@@ -116,15 +116,29 @@ class DekodiNREIExporter(BaseExporter):
'PTNo15': p.full_id or '',
})
elif p.provider and p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data)
pi = p.info_data or {}
try:
if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict):
details = pi["latest_charge"]["payment_method_details"]
card = details.get("card", {})
elif pi.get("charges") and pi["charges"]["data"]:
details = pi["charges"]["data"][0].get("payment_method_details", {})
card = details.get("card", {})
else:
details = pi["source"]
card = pi["source"]["card"]
except:
details = {}
card = {}
payments.append({
'PTID': '81',
'PTN': 'Stripe',
'PTNo1': p.info_data.get("id") or '',
'PTNo5': src.get("card", {}).get("last4") or '',
'PTNo1': pi.get("id") or '',
'PTNo5': card.get("last4", ""),
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
'PTNo10': details.get('owner', {}).get('verified_name') or details.get('owner', {}).get('name') or '',
'PTNo15': p.full_id or '',
})
else:
+11
View File
@@ -35,6 +35,7 @@
import hashlib
import ipaddress
import logging
from django import forms
from django.conf import settings
@@ -44,10 +45,13 @@ from django.contrib.auth.password_validation import (
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix.base.metrics import pretix_failed_logins
from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
from pretix.helpers.http import get_client_ip
logger = logging.getLogger(__name__)
class LoginForm(forms.Form):
"""
@@ -55,6 +59,7 @@ class LoginForm(forms.Form):
username/password logins.
"""
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
origin = forms.CharField(widget=forms.HiddenInput, required=False)
error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."),
@@ -104,12 +109,16 @@ class LoginForm(forms.Form):
rc = get_redis_connection("redis")
cnt = rc.get(self.ratelimit_key)
if cnt and int(cnt) > 10:
pretix_failed_logins.inc(1, reason="ratelimit")
logger.info("Backend login rejected due to rate limit.")
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if self.user_cache is None:
if self.ratelimit_key:
rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 300)
logger.info("Backend login invalid.")
pretix_failed_logins.inc(1, reason="invalid")
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'
@@ -131,6 +140,8 @@ class LoginForm(forms.Form):
If the given user may log in, this method should return None.
"""
if not user.is_active:
logger.info("Backend login rejected due to user inactive.")
pretix_failed_logins.inc(1, reason="inactive")
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
+6 -1
View File
@@ -268,7 +268,10 @@ def metric_values():
dkey = key.decode("utf-8")
splitted = dkey.split("{", 2)
value = float(value.decode("utf-8"))
metrics[splitted[0]]["{" + splitted[1]] = value
if len(splitted) == 1:
metrics[splitted[0]][""] = value
else:
metrics[splitted[0]]["{" + splitted[1]] = value
# Aliases
aliases = {
@@ -314,3 +317,5 @@ pretix_task_runs_total = Counter("pretix_task_runs_total", "Total calls to a cel
["task_name", "status"])
pretix_task_duration_seconds = Histogram("pretix_task_duration_seconds", "Call time of a celery task",
["task_name"])
pretix_successful_logins = Counter("pretix_logins_successful", "Successful logins", [])
pretix_failed_logins = Counter("pretix_logins_failed", "Failed logins", ["reason"])
@@ -0,0 +1,48 @@
# Generated by Django 4.2.10 on 2024-03-15 09:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0257_item_default_price_not_null"),
]
operations = [
migrations.AddField(
model_name="order",
name="organizer",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="pretixbase.organizer",
),
),
migrations.AddField(
model_name="orderposition",
name="organizer",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="order_positions",
to="pretixbase.organizer",
),
),
migrations.AddConstraint(
model_name="order",
constraint=models.UniqueConstraint(
fields=("organizer", "code"), name="order_organizer_code_uniq"
),
),
migrations.AddConstraint(
model_name="orderposition",
constraint=models.UniqueConstraint(
models.F("organizer"),
models.F("secret"),
name="orderposition_organizer_secret_uniq",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-04-02 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0258_uniq_indx"),
]
operations = [
migrations.AddField(
model_name="team",
name="require_2fa",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0259_team_require_2fa"),
]
operations = [
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
),
]
@@ -0,0 +1,48 @@
# Generated by Django 4.2.10 on 2024-04-02 15:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0260_alter_reusablemedium_index_together"),
]
operations = [
migrations.CreateModel(
name="UserKnownLoginSource",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("agent_type", models.CharField(max_length=255, null=True)),
("device_type", models.CharField(max_length=255, null=True)),
("os_type", models.CharField(max_length=255, null=True)),
(
"country",
pretix.helpers.countries.FastCountryField(
countries=pretix.helpers.countries.CachedCountries,
max_length=2,
null=True,
),
),
("last_seen", models.DateTimeField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="known_login_sources",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
+287
View File
@@ -0,0 +1,287 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import csv
import datetime
import io
import re
from decimal import Decimal, DecimalException
from django.core.exceptions import ValidationError
from django.core.validators import validate_integer
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.i18n import LazyLocaleException
from pretix.base.models import SubEvent
class DataImportError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def parse_csv(file, length=None, mode="strict", charset=None):
file.seek(0)
data = file.read(length)
if not charset:
try:
import chardet
charset = chardet.detect(data)['encoding']
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
try:
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
except csv.Error:
return None
if dialect is None:
return None
reader = csv.DictReader(io.StringIO(data), dialect=dialect)
return reader
class ImportColumn:
@property
def identifier(self):
"""
Unique, internal name of the column.
"""
raise NotImplementedError
@property
def verbose_name(self):
"""
Human-readable description of the column
"""
raise NotImplementedError
@property
def initial(self):
"""
Initial value for the form component
"""
return None
@property
def default_value(self):
"""
Internal default value for the assignment of this column. Defaults to ``empty``. Return ``None`` to disable this
option.
"""
return 'empty'
@property
def default_label(self):
"""
Human-readable description of the default assignment of this column, defaults to "Keep empty".
"""
return gettext_lazy('Keep empty')
def __init__(self, event):
self.event = event
def static_choices(self):
"""
This will be called when rendering the form component and allows you to return a list of values that can be
selected by the user statically during import.
:return: list of 2-tuples of strings
"""
return []
def resolve(self, settings, record):
"""
This method will be called to get the raw value for this field, usually by either using a static value or
inspecting the CSV file for the assigned header. You usually do not need to implement this on your own,
the default should be fine.
"""
k = settings.get(self.identifier, self.default_value)
if k == self.default_value:
return None
elif k.startswith('csv:'):
return record.get(k[4:], None) or None
elif k.startswith('static:'):
return k[7:]
raise ValidationError(_('Invalid setting for column "{header}".').format(header=self.verbose_name))
def clean(self, value, previous_values):
"""
Allows you to validate the raw input value for your column. Raise ``ValidationError`` if the value is invalid.
You do not need to include the column or row name or value in the error message as it will automatically be
included.
:param value: Contains the raw value of your column as returned by ``resolve``. This can usually be ``None``,
e.g. if the column is empty or does not exist in this row.
:param previous_values: Dictionary containing the validated values of all columns that have already been validated.
"""
return value
def assign(self, value, obj, **kwargs):
"""
This will be called to perform the actual import. You are supposed to set attributes on the ``obj`` or other
related objects that get passed in based on the input ``value``. This is called *before* the actual database
transaction, so the input objects do not yet have a primary key. If you want to create related objects, you
need to place them into some sort of internal queue and persist them when ``save`` is called.
"""
pass
def save(self, obj):
"""
This will be called to perform the actual import. This is called inside the actual database transaction and the
input object ``obj`` has already been saved to the database.
"""
pass
@property
def timezone(self):
return self.event.timezone
def i18n_flat(l):
if isinstance(l.data, dict):
return l.data.values()
return [l.data]
class BooleanColumnMixin:
default_value = None
initial = "static:false"
def static_choices(self):
return (
("false", _("No")),
("true", _("Yes")),
)
def clean(self, value, previous_values):
if not value:
return False
if value.lower() in ("true", "1", "yes", _("Yes").lower()):
return True
elif value.lower() in ("false", "0", "no", _("No").lower()):
return False
else:
raise ValidationError(_("Could not parse {value} as a yes/no value.").format(value=value))
class DatetimeColumnMixin:
def clean(self, value, previous_values):
if not value:
return
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.timezone)
return d
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
class DecimalColumnMixin:
def clean(self, value, previous_values):
if value not in (None, ''):
value = formats.sanitize_separators(re.sub(r'[^0-9.,-]', '', value))
try:
value = Decimal(value)
except (DecimalException, TypeError):
raise ValidationError(_('You entered an invalid number.'))
return value
class IntegerColumnMixin:
def clean(self, value, previous_values):
if value is not None:
validate_integer(value)
return int(value)
class SubeventColumnMixin:
def __init__(self, *args, **kwargs):
self._subevent_cache = {}
super().__init__(*args, **kwargs)
@cached_property
def subevents(self):
return list(self.event.subevents.filter(active=True).order_by('date_from'))
def static_choices(self):
return [
(str(p.pk), str(p)) for p in self.subevents
]
def clean(self, value, previous_values):
if value in self._subevent_cache:
return self._subevent_cache[value]
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone)
try:
se = self.event.subevents.get(
active=True,
date_from__gt=d - datetime.timedelta(seconds=1),
date_from__lt=d + datetime.timedelta(seconds=1),
)
self._subevent_cache[value] = se
return se
except SubEvent.DoesNotExist:
raise ValidationError(pgettext("subevent", "No matching date was found."))
except SubEvent.MultipleObjectsReturned:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
except (ValueError, TypeError):
continue
matches = [
p for p in self.subevents
if str(p.pk) == value or any(
(v and v == value) for v in i18n_flat(p.name)) or p.date_from.isoformat() == value
]
if len(matches) == 0:
raise ValidationError(pgettext("subevent", "No matching date was found."))
if len(matches) > 1:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
self._subevent_cache[value] = matches[0]
return matches[0]
@@ -19,17 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import datetime
import re
from collections import defaultdict
from decimal import Decimal, DecimalException
import pycountry
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.db.models import Q
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
@@ -42,9 +38,13 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
i18n_flat,
)
from pretix.base.models import (
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
QuestionOption, Seat, SubEvent,
QuestionOption, Seat,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -53,99 +53,6 @@ from pretix.base.settings import (
from pretix.base.signals import order_import_columns
class ImportColumn:
@property
def identifier(self):
"""
Unique, internal name of the column.
"""
raise NotImplementedError
@property
def verbose_name(self):
"""
Human-readable description of the column
"""
raise NotImplementedError
@property
def initial(self):
"""
Initial value for the form component
"""
return None
@property
def default_value(self):
"""
Internal default value for the assignment of this column. Defaults to ``empty``. Return ``None`` to disable this
option.
"""
return 'empty'
@property
def default_label(self):
"""
Human-readable description of the default assignment of this column, defaults to "Keep empty".
"""
return gettext_lazy('Keep empty')
def __init__(self, event):
self.event = event
def static_choices(self):
"""
This will be called when rendering the form component and allows you to return a list of values that can be
selected by the user statically during import.
:return: list of 2-tuples of strings
"""
return []
def resolve(self, settings, record):
"""
This method will be called to get the raw value for this field, usually by either using a static value or
inspecting the CSV file for the assigned header. You usually do not need to implement this on your own,
the default should be fine.
"""
k = settings.get(self.identifier, self.default_value)
if k == self.default_value:
return None
elif k.startswith('csv:'):
return record.get(k[4:], None) or None
elif k.startswith('static:'):
return k[7:]
raise ValidationError(_('Invalid setting for column "{header}".').format(header=self.verbose_name))
def clean(self, value, previous_values):
"""
Allows you to validate the raw input value for your column. Raise ``ValidationError`` if the value is invalid.
You do not need to include the column or row name or value in the error message as it will automatically be
included.
:param value: Contains the raw value of your column as returned by ``resolve``. This can usually be ``None``,
e.g. if the column is empty or does not exist in this row.
:param previous_values: Dictionary containing the validated values of all columns that have already been validated.
"""
return value
def assign(self, value, order, position, invoice_address, **kwargs):
"""
This will be called to perform the actual import. You are supposed to set attributes on the ``order``, ``position``,
or ``invoice_address`` objects based on the input ``value``. This is called *before* the actual database
transaction, so these three objects do not yet have a primary key. If you want to create related objects, you
need to place them into some sort of internal queue and persist them when ``save`` is called.
"""
pass
def save(self, order):
"""
This will be called to perform the actual import. This is called inside the actual database transaction and the
input object ``order`` has already been saved to the database.
"""
pass
class EmailColumn(ImportColumn):
identifier = 'email'
verbose_name = gettext_lazy('E-mail address')
@@ -182,74 +89,20 @@ class PhoneColumn(ImportColumn):
order.phone = value
class SubeventColumn(ImportColumn):
class SubeventColumn(SubeventColumnMixin, ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
default_value = None
def __init__(self, *args, **kwargs):
self._subevent_cache = {}
super().__init__(*args, **kwargs)
@cached_property
def subevents(self):
return list(self.event.subevents.filter(active=True).order_by('date_from'))
def static_choices(self):
return [
(str(p.pk), str(p)) for p in self.subevents
]
def clean(self, value, previous_values):
if not value:
raise ValidationError(pgettext("subevent", "You need to select a date."))
if value in self._subevent_cache:
return self._subevent_cache[value]
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone)
try:
se = self.event.subevents.get(
active=True,
date_from__gt=d - datetime.timedelta(seconds=1),
date_from__lt=d + datetime.timedelta(seconds=1),
)
self._subevent_cache[value] = se
return se
except SubEvent.DoesNotExist:
raise ValidationError(pgettext("subevent", "No matching date was found."))
except SubEvent.MultipleObjectsReturned:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
except (ValueError, TypeError):
continue
matches = [
p for p in self.subevents
if str(p.pk) == value or any(
(v and v == value) for v in i18n_flat(p.name)) or p.date_from.isoformat() == value
]
if len(matches) == 0:
raise ValidationError(pgettext("subevent", "No matching date was found."))
if len(matches) > 1:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
self._subevent_cache[value] = matches[0]
return matches[0]
return super().clean(value, previous_values)
def assign(self, value, order, position, invoice_address, **kwargs):
position.subevent = value
def i18n_flat(l):
if isinstance(l.data, dict):
return l.data.values()
return [l.data]
class ItemColumn(ImportColumn):
identifier = 'item'
verbose_name = gettext_lazy('Product')
@@ -572,20 +425,11 @@ class AttendeeState(ImportColumn):
position.state = value or ''
class Price(ImportColumn):
class Price(DecimalColumnMixin, ImportColumn):
identifier = 'price'
verbose_name = gettext_lazy('Price')
default_label = gettext_lazy('Calculate from product')
def clean(self, value, previous_values):
if value not in (None, ''):
value = formats.sanitize_separators(re.sub(r'[^0-9.,-]', '', value))
try:
value = Decimal(value)
except (DecimalException, TypeError):
raise ValidationError(_('You entered an invalid number.'))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
if value is None:
p = get_price(position.item, position.variation, position.voucher, subevent=position.subevent,
@@ -649,48 +493,18 @@ class Locale(ImportColumn):
order.locale = value
class ValidFrom(ImportColumn):
class ValidFrom(DatetimeColumnMixin, ImportColumn):
identifier = 'valid_from'
verbose_name = gettext_lazy('Valid from')
def clean(self, value, previous_values):
if not value:
return
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
def assign(self, value, order, position, invoice_address, **kwargs):
position.valid_from = value
class ValidUntil(ImportColumn):
class ValidUntil(DatetimeColumnMixin, ImportColumn):
identifier = 'valid_until'
verbose_name = gettext_lazy('Valid until')
def clean(self, value, previous_values):
if not value:
return
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
def assign(self, value, order, position, invoice_address, **kwargs):
position.valid_until = value
@@ -849,7 +663,7 @@ class CustomerColumn(ImportColumn):
order.customer = value
def get_all_columns(event):
def get_order_import_columns(event):
default = []
if event.has_subevents:
default.append(SubeventColumn(event))
+378
View File
@@ -0,0 +1,378 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pretix.base.modelimport import (
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
IntegerColumnMixin, i18n_flat,
)
from pretix.base.models import ItemVariation, Quota, Seat, Voucher
from pretix.base.signals import voucher_import_columns
class CodeColumn(ImportColumn):
identifier = 'code'
verbose_name = gettext_lazy('Voucher code')
default_value = None
def __init__(self, *args):
self._cached = set()
super().__init__(*args)
def clean(self, value, previous_values):
if value:
MinLengthValidator(5)(value)
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
raise ValidationError(_('A voucher with this code already exists.'))
self._cached.add(value)
return value
def assign(self, value, obj: Voucher, **kwargs):
obj.code = value
class SubeventColumn(ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
def assign(self, value, obj: Voucher, **kwargs):
obj.subevent = value
class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'max_usages'
verbose_name = gettext_lazy('Maximum usages')
initial = "static:1"
def static_choices(self):
return [
("1", "1")
]
def assign(self, value, obj: Voucher, **kwargs):
obj.max_usages = value if value is not None else 1
class MinUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'min_usages'
verbose_name = gettext_lazy('Minimum usages')
initial = "static:1"
def static_choices(self):
return [
("1", "1")
]
def assign(self, value, obj: Voucher, **kwargs):
obj.min_usages = value if value is not None else 1
class BudgetColumn(DecimalColumnMixin, ImportColumn):
identifier = 'budget'
verbose_name = gettext_lazy('Maximum discount budget')
def assign(self, value, obj: Voucher, **kwargs):
obj.budget = value
class ValidUntilColumn(DatetimeColumnMixin, ImportColumn):
identifier = 'valid_until'
verbose_name = gettext_lazy('Valid until')
def assign(self, value, obj: Voucher, **kwargs):
obj.valid_until = value
class BlockQuotaColumn(BooleanColumnMixin, ImportColumn):
identifier = 'block_quota'
verbose_name = gettext_lazy('Reserve ticket from quota')
def assign(self, value, obj: Voucher, **kwargs):
obj.block_quota = value
class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
identifier = 'allow_ignore_quota'
verbose_name = gettext_lazy('Allow to bypass quota')
def assign(self, value, obj: Voucher, **kwargs):
obj.allow_ignore_quota = value
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price mode')
default_value = None
initial = 'static:none'
def static_choices(self):
return Voucher.PRICE_MODES
def clean(self, value, previous_values):
d = dict(Voucher.PRICE_MODES)
reverse = {v: k for k, v in Voucher.PRICE_MODES}
if value in d:
return value
elif value in reverse:
return reverse[value]
else:
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
value=value, options=', '.join(d.keys())
))
def assign(self, value, voucher: Voucher, **kwargs):
voucher.price_mode = value
class ValueColumn(DecimalColumnMixin, ImportColumn):
identifier = 'value'
verbose_name = gettext_lazy('Voucher value')
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."))
return value
def assign(self, value, obj: Voucher, **kwargs):
obj.value = value or Decimal("0.00")
class ItemColumn(ImportColumn):
identifier = 'item'
verbose_name = gettext_lazy('Product')
@cached_property
def items(self):
return list(self.event.items.filter(active=True))
def static_choices(self):
return [
(str(p.pk), str(p)) for p in self.items
]
def clean(self, value, previous_values):
if not value:
return
matches = [
p for p in self.items
if str(p.pk) == value or (p.internal_name and p.internal_name == value) or any(
(v and v == value) for v in i18n_flat(p.name))
]
if len(matches) == 0:
raise ValidationError(_("No matching product was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching products were found."))
return matches[0]
def assign(self, value, voucher, **kwargs):
voucher.item = value
class VariationColumn(ImportColumn):
identifier = 'variation'
verbose_name = gettext_lazy('Product variation')
@cached_property
def items(self):
return list(ItemVariation.objects.filter(
active=True, item__active=True, item__event=self.event
).select_related('item'))
def static_choices(self):
return [
(str(p.pk), '{} {}'.format(p.item, p.value)) for p in self.items
]
def clean(self, value, previous_values):
if value:
matches = [
p for p in self.items
if (str(p.pk) == value or any((v and v == value) for v in i18n_flat(p.value))) and p.item_id == previous_values['item'].pk
]
if len(matches) == 0:
raise ValidationError(_("No matching variation was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching variations were found."))
return matches[0]
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.variation = value
class QuotaColumn(ImportColumn):
identifier = 'quota'
verbose_name = gettext_lazy('Quota')
@cached_property
def quotas(self):
return list(Quota.objects.filter(
event=self.event
))
def static_choices(self):
return [
(str(q.pk), q.name) for q in self.quotas
]
def clean(self, value, previous_values):
if value:
if previous_values.get('item'):
raise ValidationError(_("You cannot specify a quota if you specified a product."))
matches = [
q for q in self.quotas
if str(q.pk) == value or any((v and v == value) for v in i18n_flat(q.name))
]
if len(matches) == 0:
raise ValidationError(_("No matching variation was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching variations were found."))
return matches[0]
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.quota = value
class SeatColumn(ImportColumn):
identifier = 'seat'
verbose_name = gettext_lazy('Seat ID')
def __init__(self, *args):
self._cached = set()
super().__init__(*args)
def clean(self, value, previous_values):
if value:
if self.event.has_subevents:
if not previous_values.get('subevent'):
raise ValidationError(_('You need to choose a date if you select a seat.'))
try:
value = Seat.objects.get(
event=self.event,
seat_guid=value,
subevent=previous_values.get('subevent')
)
except Seat.MultipleObjectsReturned:
raise ValidationError(_('Multiple matching seats were found.'))
except Seat.DoesNotExist:
raise ValidationError(_('No matching seat was found.'))
if not value.is_available() or value in self._cached:
raise ValidationError(
_('The seat you selected has already been taken. Please select a different seat.'))
if previous_values.get("quota"):
raise ValidationError(_('You need to choose a specific product if you select a seat.'))
if previous_values.get('max_usages', 1) > 1 or previous_values.get('min_usages', 1) > 1:
raise ValidationError(_('Seat-specific vouchers can only be used once.'))
if previous_values.get("item") and value.product != previous_values.get("item"):
raise ValidationError(
_('You need to choose the product "{prod}" for this seat.').format(prod=value.product)
)
self._cached.add(value)
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.seat = value
class TagColumn(ImportColumn):
identifier = 'tag'
verbose_name = gettext_lazy('Tag')
def assign(self, value, voucher: Voucher, **kwargs):
voucher.tag = value or ''
class CommentColumn(ImportColumn):
identifier = 'comment'
verbose_name = gettext_lazy('Comment')
def assign(self, value, voucher: Voucher, **kwargs):
voucher.comment = value or ''
class ShowHiddenItemsColumn(BooleanColumnMixin, ImportColumn):
identifier = 'show_hidden_items'
verbose_name = gettext_lazy('Shows hidden products that match this voucher')
initial = "static:true"
def assign(self, value, obj: Voucher, **kwargs):
obj.show_hidden_items = value
class AllAddonsIncludedColumn(BooleanColumnMixin, ImportColumn):
identifier = 'all_addons_included'
verbose_name = gettext_lazy('Offer all add-on products for free when redeeming this voucher')
def assign(self, value, obj: Voucher, **kwargs):
obj.all_addons_included = value
class AllBundlesIncludedColumn(BooleanColumnMixin, ImportColumn):
identifier = 'all_bundles_included'
verbose_name = gettext_lazy('Include all bundled products without a designated price when redeeming this voucher')
def assign(self, value, obj: Voucher, **kwargs):
obj.all_bundles_included = value
def get_voucher_import_columns(event):
default = []
if event.has_subevents:
default.append(SubeventColumn(event))
default += [
CodeColumn(event),
MaxUsagesColumn(event),
MinUsagesColumn(event),
BudgetColumn(event),
ValidUntilColumn(event),
BlockQuotaColumn(event),
AllowIgnoreQuotaColumn(event),
PriceModeColumn(event),
ValueColumn(event),
ItemColumn(event),
VariationColumn(event),
QuotaColumn(event),
SeatColumn(event),
TagColumn(event),
CommentColumn(event),
ShowHiddenItemsColumn(event),
AllAddonsIncludedColumn(event),
AllBundlesIncludedColumn(event),
]
for recv, resp in voucher_import_columns.send(sender=event):
default += resp
return default
+10
View File
@@ -56,6 +56,7 @@ from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
from ...helpers.countries import FastCountryField
from ...helpers.u2f import pub_key_from_der, websafe_decode
from .base import LoggingMixin
@@ -579,6 +580,15 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
self.save(update_fields=['session_token'])
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True)
device_type = models.CharField(max_length=255, null=True, blank=True)
os_type = models.CharField(max_length=255, null=True, blank=True)
country = FastCountryField(null=True, blank=True)
last_seen = models.DateTimeField()
class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT)
date_start = models.DateTimeField(auto_now_add=True)
-1
View File
@@ -122,7 +122,6 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
indexes = [
models.Index(fields=("identifier", "type", "organizer")),
models.Index(fields=("updated", "id")),
]
ordering = "identifier", "type", "organizer"
+45 -3
View File
@@ -188,6 +188,14 @@ class Order(LockModel, LoggedModel):
default=False,
)
testmode = models.BooleanField(default=False)
organizer = models.ForeignKey(
# Redundant foreign key, but is required for a uniqueness constraint
"Organizer",
related_name="orders",
on_delete=models.CASCADE,
null=True,
blank=True,
)
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
@@ -286,6 +294,9 @@ class Order(LockModel, LoggedModel):
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
]
constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
]
def __str__(self):
return self.full_code
@@ -499,6 +510,10 @@ class Order(LockModel, LoggedModel):
self.set_expires()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'expires'}.union(kwargs['update_fields'])
if not self.organizer_id:
self.organizer_id = self.event.organizer_id
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'organizer'}.union(kwargs['update_fields'])
is_new = not self.pk
update_fields = kwargs.get('update_fields', [])
@@ -2356,6 +2371,14 @@ class OrderPosition(AbstractPosition):
"""
positionid = models.PositiveIntegerField(default=1)
organizer = models.ForeignKey(
# Redundant foreign key, but is required for a uniqueness constraint
"Organizer",
related_name="order_positions",
on_delete=models.CASCADE,
null=True,
blank=True,
)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
@@ -2429,6 +2452,9 @@ class OrderPosition(AbstractPosition):
verbose_name = _("Order position")
verbose_name_plural = _("Order positions")
ordering = ("positionid", "id")
constraints = [
models.UniqueConstraint("organizer", "secret", name="orderposition_organizer_secret_uniq")
]
@cached_property
def sort_key(self):
@@ -2498,7 +2524,8 @@ class OrderPosition(AbstractPosition):
op = OrderPosition(order=order)
for f in AbstractPosition._meta.fields:
if f.name == 'addon_to':
setattr(op, f.name, cp_mapping.get(cartpos.addon_to_id))
if cartpos.addon_to_id:
setattr(op, f.name, cp_mapping[cartpos.addon_to_id])
else:
setattr(op, f.name, getattr(cartpos, f.name))
op._calculate_tax()
@@ -2518,6 +2545,9 @@ class OrderPosition(AbstractPosition):
op.valid_from = valid_from
op.valid_until = valid_until
if op.is_bundled and not op.addon_to_id:
raise ValueError("Bundled cart position without parent does not make sense.")
op.positionid = i + 1
op.save()
ops.append(op)
@@ -2552,10 +2582,10 @@ class OrderPosition(AbstractPosition):
self.item.id, self.variation.id if self.variation else 0, self.order_id
)
def _calculate_tax(self, tax_rule=None):
def _calculate_tax(self, tax_rule=None, invoice_address=None):
self.tax_rule = tax_rule or self.item.tax_rule
try:
ia = self.order.invoice_address
ia = invoice_address or self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if self.tax_rule:
@@ -2585,6 +2615,10 @@ class OrderPosition(AbstractPosition):
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'secret'}.union(kwargs['update_fields'])
if not self.organizer_id:
self.organizer_id = self.order.event.organizer_id
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'organizer'}.union(kwargs['update_fields'])
if not self.blocked and self.blocked is not None:
self.blocked = None
if 'update_fields' in kwargs:
@@ -2932,6 +2966,14 @@ class CartPosition(AbstractPosition):
self.item.id, self.variation.id if self.variation else 0, self.cart_id
)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# invalidate cached values of cached properties that likely have changed
try:
del self.sort_key
except AttributeError:
pass
@property
def tax_value(self):
net = round_decimal(self.price - (self.price * (1 - 100 / (100 + self.tax_rate))),
+6
View File
@@ -263,6 +263,12 @@ class Team(LoggedModel):
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
require_2fa = models.BooleanField(
default=False, verbose_name=_("Require all members of this team to use two-factor authentication"),
help_text=_("If you turn this on, all members of the team will be required to either set up two-factor "
"authentication or leave the team. The setting may take a few minutes to become effective for "
"all users.")
)
can_create_events = models.BooleanField(
default=False,
-3
View File
@@ -517,9 +517,6 @@ class Voucher(LoggedModel):
if item and seat.product != item:
raise ValidationError(_('You need to choose the product "{prod}" for this seat.').format(prod=seat.product))
if not seat.is_available(ignore_voucher_id=pk):
raise ValidationError(_('The seat "{id}" is already sold or currently blocked.').format(id=seat.seat_guid))
return seat
def save(self, *args, **kwargs):
+5 -4
View File
@@ -935,10 +935,6 @@ class Renderer:
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
font = o['fontfamily']
if o['bold']:
font += ' B'
if o['italic']:
font += ' I'
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
# should not have access to.
@@ -946,6 +942,11 @@ class Renderer:
logger.warning(f'Unauthorized use of font "{font}"')
font = 'Open Sans'
if o['bold']:
font += ' B'
if o['italic']:
font += ' I'
try:
ad = getAscentDescent(font, float(o['fontsize']))
except KeyError: # font not known, fall back
+1 -1
View File
@@ -1358,7 +1358,7 @@ class CartManager:
return err
def recompute_final_prices_and_taxes(self):
positions = sorted(list(self.positions), key=lambda op: -(op.addon_to_id or 0))
positions = sorted(list(self.positions), key=lambda cp: (-(cp.addon_to_id or 0), cp.pk))
diff = Decimal('0.00')
for cp in positions:
if cp.listed_price is None:
+7
View File
@@ -31,6 +31,7 @@ from pretix.base.models import CachedCombinedTicket, CachedTicket
from pretix.base.models.customers import CustomerSSOGrant
from ..models import CachedFile, CartPosition, InvoiceAddress
from ..models.auth import UserKnownLoginSource
from ..signals import periodic_task
@@ -75,3 +76,9 @@ def clearsessions(sender, **kwargs):
@scopes_disabled()
def clear_oidc_data(sender, **kwargs):
CustomerSSOGrant.objects.filter(expires__lt=now() - timedelta(days=14)).delete()
@receiver(signal=periodic_task)
@scopes_disabled()
def clear_old_login_sources(sender, **kwargs):
UserKnownLoginSource.objects.filter(last_seen__lt=now() - timedelta(days=365)).delete()
@@ -19,9 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import csv
import io
from decimal import Decimal
from typing import List
from django.conf import settings as django_settings
from django.core.exceptions import ValidationError
@@ -29,13 +28,15 @@ from django.db import transaction
from django.utils.timezone import now
from django.utils.translation import gettext as _
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.i18n import language
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
from pretix.base.modelimport_orders import get_order_import_columns
from pretix.base.modelimport_vouchers import get_voucher_import_columns
from pretix.base.models import (
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition,
User,
User, Voucher,
)
from pretix.base.models.orders import Transaction
from pretix.base.orderimport import get_all_columns
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.locking import lock_objects
from pretix.base.services.tasks import ProfiledEventTask
@@ -43,47 +44,36 @@ from pretix.base.signals import order_paid, order_placed
from pretix.celery_app import app
class DataImportError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def parse_csv(file, length=None, mode="strict", charset=None):
file.seek(0)
data = file.read(length)
if not charset:
try:
import chardet
charset = chardet.detect(data)['encoding']
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict):
try:
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
except csv.Error:
return None
if dialect is None:
return None
reader = csv.DictReader(io.StringIO(data), dialect=dialect)
return reader
def setif(record, obj, attr, setting):
if setting.startswith('csv:'):
setattr(obj, attr, record[setting[4:]] or '')
parsed = parse_csv(cf.file, charset=charset)
except UnicodeDecodeError as e:
raise DataImportError(
_(
'Error decoding special characters in your file: {message}').format(
message=str(e)
)
)
data = []
for i, record in enumerate(parsed):
if not any(record.values()):
continue
values = {}
for c in cols:
val = c.resolve(settings, record)
if isinstance(val, str):
val = val.strip()
try:
values[c.identifier] = c.clean(val, values)
except ValidationError as e:
raise DataImportError(
_(
'Error while importing value "{value}" for column "{column}" in line "{line}": {message}').format(
value=val if val is not None else '', column=c.verbose_name, line=i + 1, message=e.message
)
)
data.append(values)
return data
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
@@ -91,45 +81,17 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
cf = CachedFile.objects.get(id=fileid)
user = User.objects.get(pk=user)
with language(locale, event.settings.region):
cols = get_all_columns(event)
try:
parsed = parse_csv(cf.file, charset=charset)
except UnicodeDecodeError as e:
raise DataImportError(
_(
'Error decoding special characters in your file: {message}').format(
message=str(e)
)
)
orders = []
order = None
data = []
# Run validation
for i, record in enumerate(parsed):
if not any(record.values()):
continue
values = {}
for c in cols:
val = c.resolve(settings, record)
if isinstance(val, str):
val = val.strip()
try:
values[c.identifier] = c.clean(val, values)
except ValidationError as e:
raise DataImportError(
_(
'Error while importing value "{value}" for column "{column}" in line "{line}": {message}').format(
value=val if val is not None else '', column=c.verbose_name, line=i + 1, message=e.message
)
)
data.append(values)
cols = get_order_import_columns(event)
data = _validate(cf, charset, cols, settings)
if settings['orders'] == 'one' and len(data) > django_settings.PRETIX_MAX_ORDER_SIZE:
raise DataImportError(
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
)
orders = []
order = None
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality…
lock_seats = []
@@ -149,16 +111,16 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
position = OrderPosition(positionid=len(order._positions) + 1)
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
position.meta_info = {}
if position.seat is not None:
lock_seats.append(position.seat)
order._positions.append(position)
position.assign_pseudonymization_id()
for c in cols:
c.assign(record.get(c.identifier), order, position, order._address)
except ImportError as e:
raise ImportError(
if position.seat is not None:
lock_seats.append(position.seat)
except (ValidationError, ImportError) as e:
raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
)
@@ -169,7 +131,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
lock_objects(lock_seats, shared_lock_objects=[event])
for s in lock_seats:
if not s.is_available():
raise ImportError(_('The seat you selected has already been taken. Please select a different seat.'))
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
save_transactions = []
for o in orders:
@@ -232,3 +194,62 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
'Please try again.'))
cf.delete()
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user, charset=None) -> None:
cf = CachedFile.objects.get(id=fileid)
user = User.objects.get(pk=user)
with language(locale, event.settings.region):
cols = get_voucher_import_columns(event)
data = _validate(cf, charset, cols, settings)
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality…
vouchers = []
lock_seats = []
for i, record in enumerate(data):
try:
voucher = Voucher(event=event)
vouchers.append(voucher)
Voucher.clean_item_properties(
record,
event,
record.get('quota'),
record.get('item'),
record.get('variation'),
block_quota=record.get('block_quota')
)
Voucher.clean_subevent(record, event)
Voucher.clean_max_usages(record, 0)
for c in cols:
c.assign(record.get(c.identifier), voucher)
if voucher.seat is not None:
lock_seats.append(voucher.seat)
except (ValidationError, ImportError) as e:
raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
)
with transaction.atomic():
# We don't support quotas here, so we only need to lock if seats are in use
if lock_seats:
lock_objects(lock_seats, shared_lock_objects=[event])
for s in lock_seats:
if not s.is_available():
raise DataImportError(
_('The seat you selected has already been taken. Please select a different seat.'))
for v in vouchers:
v.save()
v.log_action(
'pretix.voucher.added',
user=user,
data={'source': 'import'}
)
for c in cols:
c.save(v)
cf.delete()
+25 -5
View File
@@ -412,6 +412,11 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
email_subject, email_template, email_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
attach_ical=order.event.settings.mail_attach_ical and (
not order.event.settings.mail_attach_ical_paid_only or
order.total == Decimal('0.00') or
order.valid_if_pending
),
invoices=[invoice] if invoice and order.event.settings.invoice_email_attachment else []
)
except SendMailException:
@@ -665,7 +670,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
deleted_positions.add(cp.pk)
cp.delete()
sorted_positions = sorted(positions, key=lambda s: -int(s.is_bundled))
sorted_positions = sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk))
for cp in sorted_positions:
cp._cached_quotas = list(cp.quotas)
@@ -876,6 +881,13 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.discount = discount
cp.save(update_fields=['price', 'discount'])
# After applying discounts, add-on positions might still have a reference to the *old* version of the
# parent position, which can screw up ordering later since the system sees inconsistent data.
by_id = {cp.pk: cp for cp in sorted_positions}
for cp in sorted_positions:
if cp.addon_to_id:
cp.addon_to = by_id[cp.addon_to_id]
new_total = sum(cp.price for cp in sorted_positions)
if old_total != new_total:
err = err or error_messages['price_changed']
@@ -884,7 +896,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
for cp in sorted_positions:
cp.expires = now_dt + timedelta(
minutes=event.settings.get('reservation_time', as_type=int))
cp.save()
cp.save(update_fields=['expires'])
if err:
raise OrderError(err)
@@ -1045,7 +1057,11 @@ def _order_placed_email(event: Event, order: Order, email_template, subject_temp
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (not event.settings.mail_attach_ical_paid_only or is_free),
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
@@ -1064,7 +1080,11 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
log_entry,
invoices=[],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (not event.settings.mail_attach_ical_paid_only or is_free),
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
@@ -1144,7 +1164,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
positions = list(
positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
)
positions.sort(key=lambda k: position_ids.index(k.pk))
positions.sort(key=lambda c: c.sort_key)
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
+2 -2
View File
@@ -514,7 +514,7 @@ def base_placeholders(sender, **kwargs):
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
lambda event: concatenation_for_salutation(name_scheme['sample']),
))
ph.append(SimpleFunctionalTextPlaceholder(
"name", ["waiting_list_entry"],
@@ -524,7 +524,7 @@ def base_placeholders(sender, **kwargs):
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
lambda event: concatenation_for_salutation(name_scheme['sample']),
))
for f, l, w in name_scheme['fields']:
+9
View File
@@ -785,6 +785,15 @@ to define additional columns that can be read during import. You are expected to
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
voucher_import_columns = EventPluginSignal()
"""
This signal is sent out if the user performs an import of vouchers from an external source. You can use this
to define additional columns that can be read during import. You are expected to return a list of instances of
``ImportColumn`` subclasses.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_event_settings = EventPluginSignal()
"""
Arguments: ``settings_dict``
+12
View File
@@ -159,6 +159,18 @@ def timeline_for_event(event, subevent=None):
})
))
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
if rd and event.settings.waiting_list_enabled:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Waiting list is disabled'),
edit_url=reverse('control:event.settings', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
}) + '#waiting-list-open'
))
if not event.has_subevents:
days = event.settings.get('mail_days_download_reminder', as_type=int)
if days is not None and event.settings.ticket_download:
@@ -22,10 +22,54 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from pretix.base.services.orderimport import get_all_columns
from pretix.base.modelimport_orders import get_order_import_columns
from pretix.base.modelimport_vouchers import get_voucher_import_columns
class ProcessForm(forms.Form):
def __init__(self, *args, **kwargs):
headers = kwargs.pop('headers')
initital = kwargs.pop('initial', {}) or {}
kwargs['initial'] = initital
columns = self.get_columns()
column_keys = {c.identifier for c in columns}
if not initital or all(k not in column_keys for k in initital.keys()):
for c in columns:
initital.setdefault(c.identifier, c.initial)
for h in headers:
if h == c.identifier or h == str(c.verbose_name):
initital[c.identifier] = 'csv:{}'.format(h)
break
super().__init__(*args, **kwargs)
header_choices = [
('csv:{}'.format(h), _('CSV column: "{name}"').format(name=h)) for h in headers
]
for c in columns:
choices = []
if c.default_value:
choices.append((c.default_value, c.default_label))
choices += header_choices
for k, v in c.static_choices():
choices.append(('static:{}'.format(k), v))
self.fields[c.identifier] = forms.ChoiceField(
label=str(c.verbose_name),
choices=choices,
widget=forms.Select(
attrs={'data-static': 'true'}
)
)
def get_columns(self):
raise NotImplementedError() # noqa
class OrdersProcessForm(ProcessForm):
orders = forms.ChoiceField(
label=_('Import mode'),
choices=(
@@ -46,29 +90,21 @@ class ProcessForm(forms.Form):
)
def __init__(self, *args, **kwargs):
headers = kwargs.pop('headers')
initital = kwargs.pop('initial', {})
self.event = kwargs.pop('event')
initital = kwargs.pop('initial', {})
initital['testmode'] = self.event.testmode
kwargs['initial'] = initital
super().__init__(*args, **kwargs)
header_choices = [
('csv:{}'.format(h), _('CSV column: "{name}"').format(name=h)) for h in headers
]
def get_columns(self):
return get_order_import_columns(self.event)
for c in get_all_columns(self.event):
choices = []
if c.default_value:
choices.append((c.default_value, c.default_label))
choices += header_choices
for k, v in c.static_choices():
choices.append(('static:{}'.format(k), v))
self.fields[c.identifier] = forms.ChoiceField(
label=str(c.verbose_name),
choices=choices,
widget=forms.Select(
attrs={'data-static': 'true'}
)
)
class VouchersProcessForm(ProcessForm):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
def get_columns(self):
return get_voucher_import_columns(self.event)
+1 -1
View File
@@ -257,7 +257,7 @@ class TeamForm(forms.ModelForm):
class Meta:
model = Team
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
fields = ['name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events',
'can_change_teams', 'can_change_organizer_settings',
'can_manage_gift_cards', 'can_manage_customers',
'can_manage_reusable_media',
+3
View File
@@ -455,9 +455,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.event.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.control.auth.user.new_source': _('A first login using {agent_type} on {os_type} from {country} has '
'been detected.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
+10 -8
View File
@@ -48,7 +48,8 @@ from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
from pretix.helpers.http import redirect_to_url
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
SessionReauthRequired, assert_session_valid,
)
@@ -67,6 +68,7 @@ class PermissionMiddleware:
"auth.forgot.recover",
"auth.invite",
"user.settings.notifications.off",
"auth.bad_origin_report",
)
EXCEPTIONS_FORCED_PW_CHANGE = (
@@ -83,6 +85,7 @@ class PermissionMiddleware:
"user.settings.2fa.confirm.totp",
"user.settings.2fa.confirm.webauthn",
"user.settings.2fa.delete",
"user.settings.2fa.leaveteams",
"auth.logout",
"user.reauth"
)
@@ -134,13 +137,12 @@ class PermissionMiddleware:
except SessionReauthRequired:
if url_name not in ('user.reauth', 'auth.logout'):
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
and url_name not in self.EXCEPTIONS_2FA and not request.user.needs_password_change:
return redirect_to_url(reverse('control:user.settings.2fa'))
except SessionPasswordChangeRequired:
if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
except Session2FASetupRequired:
if url_name not in self.EXCEPTIONS_2FA:
return redirect_to_url(reverse('control:user.settings.2fa'))
if 'event' in url.kwargs and 'organizer' in url.kwargs:
if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-':
@@ -58,5 +58,6 @@
{{ poweredby }} {# removing or hiding this might be in violation of pretix' license #}
</footer>
</div>
<script type="text/javascript" src="{% static "pretixcontrol/js/auth.js" %}"></script>
</body>
</html>
@@ -46,5 +46,7 @@
{% endif %}
</div>
</form>
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
{% endblock %}
@@ -0,0 +1,13 @@
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello,
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
If this was you, you can safely ignore this email.
If this was not you, we recommend that you change your password in your account settings:
{{ url }}
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -335,7 +335,7 @@
{% bootstrap_field sform.max_items_per_order layout="control" %}
{% bootstrap_field sform.redirect_to_checkout_directly layout="control" %}
</fieldset>
<fieldset>
<fieldset id="waiting-list">
<legend>{% trans "Waiting list" %}</legend>
<div class="alert alert-info">
{% blocktrans trimmed %}
@@ -18,6 +18,7 @@
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.require_2fa layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Organizer permissions" %}</legend>
@@ -0,0 +1,30 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Two-factor authentication" %}{% endblock %}
{% block content %}
<h1>{% trans "Leave teams that require two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>
<strong>{% trans "Do you really want to leave the following teams?" %}</strong>
</p>
<ul>
{% for t in obligatory_teams %}
<li>
{% blocktrans trimmed with team=t.name organizer=t.organizer.name %}
Team "{{ team }}" of organizer "{{ organizer }}"
{% endblocktrans %}
</li>
{% endfor %}
</ul>
<div class="form-group submit-group">
<a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Leave" %}
</button>
</div>
</form>
{% endblock %}
@@ -11,23 +11,55 @@
smartphone or a hardware token generator and that changes on a regular basis.
{% endblocktrans %}
</p>
{% if settings.PRETIX_OBLIGATORY_2FA %}
{% if obligatory and not user.require_2fa %}
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Obligatory usage of two-factor authentication" %}</h3>
<h3 class="panel-title">
<span class="fa fa-warning"></span>
{% trans "Obligatory usage of two-factor authentication" %}
</h3>
</div>
<div class="panel-body">
{% if obligatory == "system" %}
<p>
<strong>{% trans "This system enforces the usage of two-factor authentication!" %}</strong>
</p>
{% elif obligatory == "staff" %}
<p>
<strong>{% trans "As an administrator, you need to use two-factor authentication." %}</strong>
</p>
{% elif obligatory == "team" %}
<p>
<strong>{% trans "You are part of one or more organizer teams that require you to use two-factor authentication." %}</strong>
</p>
<ul>
{% for t in obligatory_teams %}
<li>
{% blocktrans trimmed with team=t.name organizer=t.organizer.name %}
Team "{{ team }}" of organizer "{{ organizer }}"
{% endblocktrans %}
</li>
{% endfor %}
</ul>
{% endif %}
<p>
<strong>{% trans "This system enforces the usage of two-factor authentication!" %}</strong>
{% if not devices %}
{% trans "Please set up at least one device below." %}
{% elif not user.require_2fa %}
{% trans "Please activate two-factor authentication using the button below." %}
{% endif %}
{% if obligatory == "team" %}
<a href="{% url "control:user.settings.2fa.leaveteams" %}">
{% blocktrans trimmed count count=obligatory_teams|length %}
Leave team instead
{% plural %}
Leave {{ count }} teams instead
{% endblocktrans %}
</a>
{% endif %}
</p>
{% if not devices %}
<p>{% trans "Please set up at least one device below." %}</p>
{% elif not user.require_2fa %}
<p>{% trans "Please activate two-factor authentication using the button below." %}</p>
{% endif %}
</div>
</div>
{% endif %}
{% if user.require_2fa %}
<div class="panel panel-success">
@@ -35,7 +67,18 @@
<h3 class="panel-title">{% trans "Two-factor status" %}</h3>
</div>
<div class="panel-body">
{% if not settings.PRETIX_OBLIGATORY_2FA %}
{% if obligatory %}
<button disabled class="btn btn-primary pull-right flip" data-toggle="tooltip"
title="{% spaceless %}{% if obligatory == "system" %}
{% trans "This system enforces the usage of two-factor authentication!" %}
{% elif obligatory == "staff" %}
{% trans "As an administrator, you need to use two-factor authentication." %}
{% elif obligatory == "team" %}
{% trans "You are part of one or more organizer teams that require you to use two-factor authentication." %}
{% endif %}{% endspaceless %}">
{% trans "Disable" %}
</button>
{% else %}
<a href="{% url "control:user.settings.2fa.disable" %}" class="btn btn-primary pull-right flip">
{% trans "Disable" %}
</a>
@@ -73,7 +116,7 @@
{% for d in devices %}
<li class="list-group-item">
<a class="btn btn-danger btn-xs pull-right flip"
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
Delete
</a>
{% if d.devicetype == "totp" %}
@@ -11,6 +11,12 @@
<button class="btn btn-default">{% trans "Send password reset email" %}</button>
</form>
{% endif %}
{% if user.require_2fa %}
<form action="{% url "control:users.emergencytoken" id=user.pk %}" method="post" class="form-inline helper-display-inline">
{% csrf_token %}
<button class="btn btn-default">{% trans "Generate 2FA emergency token" %}</button>
</form>
{% endif %}
<form action="{% url "control:users.impersonate" id=user.pk %}" method="post" class="form-inline helper-display-inline">
{% csrf_token %}
<button class="btn btn-default">{% trans "Impersonate user" %}</button>
@@ -0,0 +1,61 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load static %}
{% load getitem %}
{% load bootstrap3 %}
{% block title %}{% trans "Import vouchers" %}{% endblock %}
{% block content %}
<h1>{% trans "Import vouchers" %}</h1>
<form action="" method="post" class="form-horizontal" data-asynctask data-asynctask-long>
{% csrf_token %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Data preview" %}</h3>
</div>
<div class="table-responsive panel-body">
<table class="table table-condensed">
<thead>
<tr>
{% for fn in parsed.fieldnames %}
<th>{{ fn }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for r in sample_rows %}
<tr>
{% for fn in parsed.fieldnames %}
<td>{{ r|getitem:fn }}</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<td class="text-center" colspan="{{ parsed.fieldnames|length }}">
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Import settings" %}</h3>
</div>
<div class="panel-body">
{% bootstrap_form_errors form %}
{% bootstrap_form form layout="horizontal" %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The import will be performed regardless of your quotas, so it will be possible to overbook your event using this option.
{% endblocktrans %}
</div>
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Perform import" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,40 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Import vouchers" %}{% endblock %}
{% block content %}
<h1>{% trans "Import vouchers" %}</h1>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
</div>
<div class="panel-body">
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
{% blocktrans trimmed %}
The uploaded file should be a CSV file with a header row. You will be able to assign the
meanings of the different columns in the next step.
{% endblocktrans %}
</p>
<div class="form-group">
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
</div>
<div class="form-group">
<label for="file">{% trans "Character set" %}: </label>
<select name="charset" class="form-control">
<option>{% trans "Detect automatically" %}</option>
{% for e in encodings %}
<option value="{{ e }}">{{ e }}</option>
{% endfor %}
</select>
</div>
<div class="clearfix"></div>
<button class="btn btn-primary pull-right flip" type="submit">
<span class="icon icon-upload"></span> {% trans "Start import" %}
</button>
</form>
</div>
</div>
{% endblock %}
@@ -77,6 +77,8 @@
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create multiple new vouchers" %}</a>
<a href="{% url "control:event.vouchers.import" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-lg"><i class="fa fa-upload"></i> {% trans "Import vouchers" %}</a>
{% endif %}
</div>
{% else %}
@@ -87,6 +89,9 @@
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i>
{% trans "Create multiple new vouchers" %}</a>
<a href="{% url "control:event.vouchers.import" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-upload"></i>
{% trans "Import vouchers" %}</a>
{% endif %}
<a href="?{% url_replace request "download" "yes" %}"
class="btn btn-default"><i class="fa fa-download"></i>
+10 -5
View File
@@ -38,13 +38,14 @@ from django.views.generic.base import RedirectView
from pretix.control.views import (
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
main, modelimport, oauth, orders, organizer, pdf, search, shredder,
subevents, typeahead, user, users, vouchers, waitinglist,
)
urlpatterns = [
re_path(r'^logout$', auth.logout, name='auth.logout'),
re_path(r'^login$', auth.login, name='auth.login'),
re_path(r'^login/bad_origin$', auth.bad_origin_report, name='auth.bad_origin_report'),
re_path(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'),
re_path(r'^register$', auth.register, name='auth.register'),
re_path(r'^invite/(?P<token>[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'),
@@ -71,8 +72,9 @@ urlpatterns = [
re_path(r'^users/impersonate/stop', users.UserImpersonateStopView.as_view(), name='users.impersonate.stop'),
re_path(r'^users/(?P<id>\d+)/$', users.UserEditView.as_view(), name='users.edit'),
re_path(r'^users/(?P<id>\d+)/reset$', users.UserResetView.as_view(), name='users.reset'),
re_path(r'^users/(?P<id>\d+)/impersonate', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
@@ -94,6 +96,7 @@ urlpatterns = [
re_path(r'^settings/oauth/apps/(?P<pk>\d+)/roll$', oauth.OAuthApplicationRollView.as_view(),
name='user.settings.oauth.app.roll'),
re_path(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
re_path(r'^settings/2fa/leaveteams$', user.User2FALeaveTeamsView.as_view(), name='user.settings.2fa.leaveteams'),
re_path(r'^settings/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'),
re_path(r'^settings/2fa/enable', user.User2FAEnableView.as_view(), name='user.settings.2fa.enable'),
re_path(r'^settings/2fa/disable', user.User2FADisableView.as_view(), name='user.settings.2fa.disable'),
@@ -346,6 +349,8 @@ urlpatterns = [
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
name='event.order.transition'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/resend$', orders.OrderResendLink.as_view(),
@@ -412,8 +417,8 @@ urlpatterns = [
re_path(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
name='event.invoice.download'),
re_path(r'^orders/overview/$', orders.OverView.as_view(), name='event.orders.overview'),
re_path(r'^orders/import/$', orderimport.ImportView.as_view(), name='event.orders.import'),
re_path(r'^orders/import/(?P<file>[^/]+)/$', orderimport.ProcessView.as_view(), name='event.orders.import.process'),
re_path(r'^orders/import/$', modelimport.OrderImportView.as_view(), name='event.orders.import'),
re_path(r'^orders/import/(?P<file>[^/]+)/$', modelimport.OrderProcessView.as_view(), name='event.orders.import.process'),
re_path(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
re_path(r'^orders/export/(?P<pk>[^/]+)/run$', orders.RunScheduledExportView.as_view(), name='event.orders.export.scheduled.run'),
re_path(r'^orders/export/(?P<pk>[^/]+)/delete$', orders.DeleteScheduledExportView.as_view(), name='event.orders.export.scheduled.delete'),
+66 -4
View File
@@ -36,7 +36,7 @@ import base64
import json
import logging
import time
from urllib.parse import quote, urlparse
from urllib.parse import quote, urljoin, urlparse
import webauthn
from django.conf import settings
@@ -47,11 +47,14 @@ from django.contrib.auth import (
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from django_otp import match_token
from webauthn.helpers import generate_challenge
@@ -60,9 +63,11 @@ from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import (
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
)
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.base.services.mail import SendMailException
from pretix.helpers.http import redirect_to_url
from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.security import handle_login_source
logger = logging.getLogger(__name__)
@@ -77,6 +82,7 @@ def process_login(request, user, keep_logged_in):
request.session['pretix_auth_long_session'] = settings.PRETIX_LONG_SESSIONS and keep_logged_in
next_url = get_auth_backends()[user.auth_backend].get_next_url(request)
if user.require_2fa:
logger.info(f"Backend login redirected to 2FA for user {user.pk}.")
request.session['pretix_auth_2fa_user'] = user.pk
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
twofa_url = reverse('control:auth.login.2fa')
@@ -84,6 +90,9 @@ def process_login(request, user, keep_logged_in):
twofa_url += '?next=' + quote(next_url)
return redirect_to_url(twofa_url)
else:
logger.info(f"Backend login successful for user {user.pk}.")
pretix_successful_logins.inc(1)
handle_login_source(user, request)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
@@ -105,6 +114,9 @@ def login(request):
return process_login(request, u, False)
b.url = b.authentication_url(request)
# Login should only happen on configured main domain
good_origin = urlparse(settings.SITE_URL).scheme + '://' + urlparse(settings.SITE_URL).hostname
backend = backenddict.get(request.GET.get('backend', 'native'), backends[0])
if not backend.visible:
backend = [b for b in backends if b.visible][0]
@@ -115,7 +127,23 @@ def login(request):
return redirect(reverse('control:index'))
if request.method == 'POST':
form = LoginForm(backend=backend, data=request.POST, request=request)
if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier:
is_valid = form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier
if form.cleaned_data.get("origin"):
form_origin = form.cleaned_data.get("origin")
if good_origin != form_origin:
logger.warning(
f"Received login form submission with unexpected origin value. "
f"Origin sent from JavaScript: {form_origin} / "
f"Expected origin from configuration: {good_origin} / "
f"HTTP Host header: {request.headers.get('Host')} / "
f"HTTP origin header: {request.headers.get('Origin')} / "
f"HTTP referer header: {request.headers.get('Referer')} / "
f"IP address: {get_client_ip(request)} / "
f"Login result: {is_valid}"
)
if is_valid:
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
else:
form = LoginForm(backend=backend, request=request)
@@ -124,9 +152,35 @@ def login(request):
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
ctx['backends'] = backends
ctx['backend'] = backend
ctx['good_origin'] = good_origin[::-1] # minimal obfuscation against standard link rewriting
ctx['bad_origin_report_url'] = urljoin(
# as an additional safeguard always use SITE_URL, not anything derived from request
settings.SITE_URL,
reverse('control:auth.bad_origin_report')
)[::-1]
return render(request, 'pretixcontrol/auth/login.html', ctx)
@csrf_exempt
@require_http_methods(["POST"])
def bad_origin_report(request):
good_origin = urlparse(settings.SITE_URL).scheme + '://' + urlparse(settings.SITE_URL).hostname
form_origin = request.POST.get("origin")
if good_origin != form_origin:
logger.warning(
f"Received report of unexpected origin value. "
f"Origin sent from JavaScript: {form_origin} / "
f"Expected origin from configuration: {good_origin} / "
f"HTTP Host header: {request.headers.get('Host')} / "
f"HTTP origin header: {request.headers.get('Origin')} / "
f"HTTP referer header: {request.headers.get('Referer')} / "
f"IP address: {get_client_ip(request)}"
)
resp = HttpResponse()
resp['Access-Control-Allow-Origin'] = '*'
return resp
def logout(request):
"""
Log the user out of the current session, then redirect to login page.
@@ -284,7 +338,7 @@ class Forgot(TemplateView):
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
except User.DoesNotExist:
logger.warning('Password reset for unregistered e-mail \"' + email + '\" requested.')
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
except SendMailException:
logger.exception('Sending password reset e-mail to \"' + email + '\" failed.')
@@ -411,6 +465,7 @@ class Login2FAView(TemplateView):
fail = True
logintime = int(request.session.get('pretix_auth_2fa_time', '1'))
if time.time() - logintime > 300:
pretix_failed_logins.inc(1, reason="2fa-timeout")
fail = True
if fail:
messages.error(request, _('Please try again.'))
@@ -443,6 +498,7 @@ class Login2FAView(TemplateView):
)
sign_count = webauthn_assertion_response.new_sign_count
if sign_count < credential_current_sign_count:
pretix_failed_logins.inc(1, reason="webauthn-replay")
raise Exception("Possible replay attack, sign count not higher")
except Exception:
if isinstance(d, U2FDevice):
@@ -460,11 +516,13 @@ class Login2FAView(TemplateView):
if webauthn_assertion_response.new_sign_count < 1:
raise Exception("Possible replay attack, sign count set")
except Exception:
pretix_failed_logins.inc(1, reason="u2f")
logger.exception('U2F login failed')
else:
valid = True
break
else:
pretix_failed_logins.inc(1, reason="webauthn")
logger.exception('Webauthn login failed')
else:
if isinstance(d, WebAuthnDevice):
@@ -476,6 +534,9 @@ class Login2FAView(TemplateView):
valid = match_token(self.user, token)
if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1)
handle_login_source(self.user, request)
auth_login(request, self.user)
request.session['pretix_auth_login_time'] = int(time.time())
del request.session['pretix_auth_2fa_user']
@@ -484,6 +545,7 @@ class Login2FAView(TemplateView):
return redirect_to_url(request.GET.get("next"))
return redirect('control:index')
else:
pretix_failed_logins.inc(1, reason="2fa")
messages.error(request, _('Invalid code, please try again.'))
return redirect('control:auth.login.2fa')
@@ -46,9 +46,13 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, TemplateView
from pretix.base.models import CachedFile
from pretix.base.services.orderimport import import_orders, parse_csv
from pretix.base.services.modelimport import (
import_orders, import_vouchers, parse_csv,
)
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.orderimport import ProcessForm
from pretix.control.forms.modelimport import (
OrdersProcessForm, VouchersProcessForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.http import redirect_to_url
@@ -64,28 +68,16 @@ ENCODINGS = (
)
class ImportView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/import_start.html'
permission = 'can_change_orders'
class BaseImportView(TemplateView):
def post(self, request, *args, **kwargs):
if 'file' not in request.FILES:
return redirect_to_url(reverse('control:event.orders.import', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}))
return redirect_to_url(request.path)
if not request.FILES['file'].name.lower().endswith('.csv'):
messages.error(request, _('Please only upload CSV files.'))
return redirect_to_url(reverse('control:event.orders.import', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}))
return redirect_to_url(request.path)
if request.FILES['file'].size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
messages.error(request, _('Please do not upload files larger than 10 MB.'))
return redirect_to_url(reverse('control:event.orders.import', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}))
return redirect_to_url(request.path)
cf = CachedFile.objects.create(
expires=now() + timedelta(days=1),
@@ -100,41 +92,47 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
else:
charset = "auto"
return redirect(reverse('control:event.orders.import.process', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'file': cf.id
}) + "?charset=" + charset)
return redirect(self.get_process_url(request, cf, charset))
def get_context_data(self, **kwargs):
return super().get_context_data(encodings=ENCODINGS)
def get_process_url(self, request, cf, charset):
raise NotImplementedError() # noqa
class ProcessView(EventPermissionRequiredMixin, AsyncAction, FormView):
permission = 'can_change_orders'
template_name = 'pretixcontrol/orders/import_process.html'
form_class = ProcessForm
task = import_orders
class BaseProcessView(AsyncAction, FormView):
known_errortypes = ['DataImportError']
@property
def settings_key(self):
raise NotImplementedError() # noqa
@property
def settings_holder(self):
raise NotImplementedError() # noqa
def get_form_kwargs(self):
k = super().get_form_kwargs()
k.update({
'event': self.request.event,
'initial': self.request.event.settings.order_import_settings,
'initial': self.settings_holder.settings.get(self.settings_key, as_type=dict),
'headers': self.parsed.fieldnames
})
return k
def form_valid(self, form):
self.request.event.settings.order_import_settings = form.cleaned_data
self.settings_holder.settings.set(self.settings_key, form.cleaned_data)
if self.request.GET.get("charset") in ENCODINGS:
charset = self.request.GET.get("charset")
else:
charset = None
return self.do(
self.request.event.pk, self.file.id, form.cleaned_data, self.request.LANGUAGE_CODE,
self.request.user.pk, charset
self.settings_holder.pk,
self.file.id,
form.cleaned_data,
self.request.LANGUAGE_CODE,
self.request.user.pk,
charset,
)
@cached_property
@@ -176,28 +174,21 @@ class ProcessView(EventPermissionRequiredMixin, AsyncAction, FormView):
return _('The import was successful.')
def get_success_url(self, value):
return reverse('control:event.orders', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
})
raise NotImplementedError() # noqa
def get_form_url(self):
raise NotImplementedError() # noqa
def dispatch(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
if not self.parsed or not self.parsed_list:
messages.error(request, _('We\'ve been unable to parse the uploaded file as a CSV file.'))
return redirect(reverse('control:event.orders.import', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}))
return redirect(self.get_form_url())
return super().dispatch(request, *args, **kwargs)
def get_error_url(self):
return reverse('control:event.orders.import.process', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
'file': self.file.id
})
return self.request.path
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -205,3 +196,89 @@ class ProcessView(EventPermissionRequiredMixin, AsyncAction, FormView):
ctx['parsed'] = self.parsed
ctx['sample_rows'] = self.parsed_list[:3]
return ctx
class OrderImportView(EventPermissionRequiredMixin, BaseImportView):
template_name = 'pretixcontrol/orders/import_start.html'
permission = 'can_change_orders'
def get_process_url(self, request, cf, charset):
return reverse('control:event.orders.import.process', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'file': cf.id
}) + "?charset=" + charset
class OrderProcessView(EventPermissionRequiredMixin, BaseProcessView):
permission = 'can_change_orders'
template_name = 'pretixcontrol/orders/import_process.html'
form_class = OrdersProcessForm
task = import_orders
settings_key = 'order_import_settings'
@property
def settings_holder(self):
return self.request.event
def get_form_kwargs(self):
k = super().get_form_kwargs()
k.update({
'event': self.request.event,
})
return k
def get_form_url(self):
return reverse('control:event.orders.import', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
})
def get_success_url(self, value):
return reverse('control:event.orders', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
})
class VoucherImportView(EventPermissionRequiredMixin, BaseImportView):
template_name = 'pretixcontrol/vouchers/import_start.html'
permission = 'can_change_vouchers'
def get_process_url(self, request, cf, charset):
return reverse('control:event.vouchers.import.process', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'file': cf.id
}) + "?charset=" + charset
class VoucherProcessView(EventPermissionRequiredMixin, BaseProcessView):
permission = 'can_change_vouchers'
template_name = 'pretixcontrol/vouchers/import_process.html'
form_class = VouchersProcessForm
task = import_vouchers
settings_key = 'voucher_import_settings'
@property
def settings_holder(self):
return self.request.event
def get_form_kwargs(self):
k = super().get_form_kwargs()
k.update({
'event': self.request.event,
})
return k
def get_form_url(self):
return reverse('control:event.vouchers.import', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
})
def get_success_url(self, value):
return reverse('control:event.vouchers', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
})
+32
View File
@@ -44,6 +44,7 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
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
@@ -323,6 +324,15 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
obj.devicetype = 'webauthn'
ctx['devices'] += objs
ctx['obligatory'] = None
if settings.PRETIX_OBLIGATORY_2FA is True:
ctx['obligatory'] = 'system'
elif settings.PRETIX_OBLIGATORY_2FA == "staff" and self.request.user.is_staff:
ctx['obligatory'] = 'staff'
elif teams := self.request.user.teams.filter(require_2fa=True).select_related('organizer'):
ctx['obligatory'] = 'team'
ctx['obligatory_teams'] = teams
return ctx
@@ -557,6 +567,28 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
}))
class User2FALeaveTeamsView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_leaveteams.html'
@transaction.atomic
def post(self, request, *args, **kwargs):
for team in self.request.user.teams.filter(require_2fa=True).select_related('organizer'):
team.members.remove(self.request.user)
team.log_action(
'pretix.team.member.removed', user=self.request.user, data={
'email': self.request.user.email,
'user': self.request.user.pk
}
)
messages.success(request, _('You have left all teams that require two-factor authentication.'))
return redirect(reverse('control:user.settings.2fa'))
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['obligatory_teams'] = self.request.user.teams.filter(require_2fa=True).select_related('organizer')
return ctx
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html'
+28
View File
@@ -30,10 +30,12 @@ from django.contrib.auth import (
from django.contrib.auth.mixins import LoginRequiredMixin
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.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import ListView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice
from hijack import signals
from pretix.base.auth import get_auth_backends
@@ -150,6 +152,32 @@ class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRe
return reverse('control:users.edit', kwargs=self.kwargs)
class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
d, __ = StaticDevice.objects.get_or_create(user=self.object, name='emergency')
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
self.object.log_action('pretix.user.settings.2fa.emergency', user=self.request.user)
messages.success(request, _(
'The emergency token for this user is "{token}". It can only be used once. Please make sure to transmit '
'this code only over an authenticated channel (other than email, if possible). Any previous emergency '
'tokens for this user remain active.'
).format(
token=token.token
))
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('control:users.edit', kwargs=self.kwargs)
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = "pretixcontrol/users/anonymize.html"
@@ -1,70 +0,0 @@
# This file is distributed under the same license as the Django package.
#
# Modified informal version by
# Raphael Michel <mail@raphaelmichel.de>, 2016
#
# Original Translators:
# André Hagenbruch, 2011-2012
# Florian Apolloner <florian@apolloner.eu>, 2011
# Jannis Vajen, 2011,2013
# Jannis Leidel <jannis@leidel.info>, 2013-2015
# Markus Holtermann <inyoka@markusholtermann.eu>, 2013,2015
msgid ""
msgstr ""
"Project-Id-Version: django\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-09 17:42+0200\n"
"PO-Revision-Date: 2016-03-18 21:22+0100\n"
"Last-Translator: Raphael Michel <mail@raphaelmichel.de>\n"
"Language-Team: German (http://www.transifex.com/django/django/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.7\n"
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Bitte wähle entweder eine Datei aus oder wähle \"Löschen\", nicht "
"beides."
msgid ""
"You are seeing this message because this HTTPS site requires a 'Referer "
"header' to be sent by your Web browser, but none was sent. This header is "
"required for security reasons, to ensure that your browser is not being "
"hijacked by third parties."
msgstr ""
"Du siehst diese Fehlermeldung da diese HTTPS-Seite einen „Referer“-Header "
"von deinem Webbrowser erwartet, aber keinen erhalten hat. Dieser Header ist "
"aus Sicherheitsgründen notwendig, um sicherzustellen, dass dein Webbrowser "
"nicht von Dritten missbraucht wird."
msgid ""
"If you have configured your browser to disable 'Referer' headers, please re-"
"enable them, at least for this site, or for HTTPS connections, or for 'same-"
"origin' requests."
msgstr ""
"Falls du deinen Webbrowser so konfiguriert hast, dass „Referer“-Header "
"nicht gesendet werden, musst du diese Funktion mindestens für diese Seite, "
"für sichere HTTPS-Verbindungen oder für „Same-Origin“-Verbindungen "
"reaktivieren."
msgid ""
"You are seeing this message because this site requires a CSRF cookie when "
"submitting forms. This cookie is required for security reasons, to ensure "
"that your browser is not being hijacked by third parties."
msgstr ""
"Du siehst Diese Nachricht, da diese Seite einen CSRF-Cookie beim Verarbeiten "
"von Formulardaten benötigt. Dieses Cookie ist aus Sicherheitsgründen "
"notwendig, um sicherzustellen, dass dein Webbrowser nicht von Dritten "
"missbraucht wird."
msgid ""
"If you have configured your browser to disable cookies, please re-enable "
"them, at least for this site, or for 'same-origin' requests."
msgstr ""
"Falls du Cookies in Ihren Webbrowser deaktiviert hast, musst du sie "
"mindestens für diese Seite oder für „Same-Origin“-Verbindungen reaktivieren."
+89
View File
@@ -20,14 +20,23 @@
# <https://www.gnu.org/licenses/>.
#
import hashlib
import logging
import time
from django.conf import settings
from django.contrib.gis.geoip2 import GeoIP2
from django.core.cache import cache
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
from geoip2.errors import AddressNotFoundError
from pretix.base.i18n import language
from pretix.base.services.mail import SendMailException, mail
from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
class SessionInvalid(Exception):
@@ -38,6 +47,14 @@ class SessionReauthRequired(Exception):
pass
class Session2FASetupRequired(Exception):
pass
class SessionPasswordChangeRequired(Exception):
pass
def get_user_agent_hash(request):
return hashlib.sha256(request.headers['User-Agent'].encode()).hexdigest()
@@ -71,6 +88,8 @@ def assert_session_valid(request):
if 'User-Agent' in request.headers:
if 'pinned_user_agent' in request.session:
if request.session.get('pinned_user_agent') != get_user_agent_hash(request):
logger.info(f"Backend session for user {request.user.pk} terminated due to user agent change. "
f"New agent: \"{request.headers['User-Agent']}\"")
raise SessionInvalid()
else:
request.session['pinned_user_agent'] = get_user_agent_hash(request)
@@ -82,9 +101,79 @@ def assert_session_valid(request):
if 'pinned_country' in request.session:
if request.session.get('pinned_country') != country:
logger.info(f"Backend session for user {request.user.pk} terminated due to country change. "
f"Old country: \"{request.session.get('pinned_countres')}\" New country: \"{country}\"")
raise SessionInvalid()
else:
request.session['pinned_country'] = country
request.session['pretix_auth_last_used'] = int(time.time())
if request.user.needs_password_change:
raise SessionPasswordChangeRequired()
force_2fa = not request.user.require_2fa and (
settings.PRETIX_OBLIGATORY_2FA is True or
(settings.PRETIX_OBLIGATORY_2FA == "staff" and request.user.is_staff) or
cache.get_or_set(
f'user_2fa_team_{request.user.pk}',
lambda: request.user.teams.filter(require_2fa=True).exists(),
timeout=300
)
)
if force_2fa:
raise Session2FASetupRequired()
return True
def handle_login_source(user, request):
from ua_parser import user_agent_parser
parsed_string = user_agent_parser.Parse(request.headers.get("User-Agent", ""))
country = None
if settings.HAS_GEOIP:
client_ip = get_client_ip(request)
hashed_client_ip = hashlib.sha256(client_ip.encode()).hexdigest()
country = cache.get_or_set(f'geoip_country_{hashed_client_ip}', lambda: _get_country(request), timeout=300)
if country == "None":
country = None
src, created = user.known_login_sources.update_or_create(
agent_type=parsed_string.get("user_agent").get("family"),
os_type=parsed_string.get("os").get("family"),
device_type=parsed_string.get("device").get("family"),
country=country,
defaults={
"last_seen": now(),
}
)
if created:
user.log_action('pretix.control.auth.user.new_source', user=user, data={
"agent_type": src.agent_type,
"os_type": src.os_type,
"device_type": src.device_type,
"country": str(src.country) if src.country else "?",
})
if user.known_login_sources.count() > 1:
# Do not send on first login or first login after introduction of this feature:
try:
with language(user.locale):
mail(
user.email,
_('Login from new source detected'),
'pretixcontrol/email/login_notice.txt',
{
'source': src,
'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=user,
locale=user.locale
)
except SendMailException:
pass # Not much we can do
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+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"
@@ -578,54 +578,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -663,32 +663,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -608,54 +608,54 @@ msgstr "توليد الرسائل …"
msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"
@@ -707,32 +707,32 @@ msgstr[3] "سيتم حجز العناصر لك في سلة التسوق لعدة
msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقائق {num}."
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "يحصل المنظم على %(currency) %(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "ستسترد %(currency)%(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "المنطقة الزمنية:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "التوقيت المحلي:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -579,54 +579,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -668,34 +668,34 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "El contingut de la cistella ja no el teniu reservat."
msgstr[1] "El contingut de la cistella ja no el teniu reservat."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -599,16 +599,16 @@ msgstr "Vytváření zpráv…"
msgid "Unknown error."
msgstr "Neznámá chyba."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "Tato barva má velmi dobrý kontrast a je velmi dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Tato barva má slušný kontrast a pravděpodobně je dostatečně dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -616,40 +616,40 @@ msgstr ""
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
"tmavší odstín."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"
@@ -694,32 +694,32 @@ msgstr[1] ""
msgstr[2] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na dalších {num} minut."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Organizátor si ponechává %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Dostanete %(currency)s %(amount)s zpět"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Zadejte částku, kterou si organizátor může ponechat."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "povinný"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Časové pásmo:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Místní čas:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+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"
@@ -579,54 +579,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -664,32 +664,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -622,54 +622,54 @@ msgstr "Opretter beskeder …"
msgid "Unknown error."
msgstr "Ukendt fejl."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"
@@ -715,40 +715,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Varerne i din kurv er reserveret for dig i et minut."
msgstr[1] "Varerne i din kurv er reserveret for dig i {num} minutter."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "fra %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "fra %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Tidszone:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Din lokaltid:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -596,17 +596,17 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und ist wahrscheinlich gut zu "
"lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -614,40 +614,40 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!"
@@ -690,32 +690,32 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Sie erhalten %(currency)s %(amount)s zurück"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Deine lokale Zeit:"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -595,17 +595,17 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und ist wahrscheinlich gut zu "
"lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -613,40 +613,40 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wähle eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!"
@@ -689,32 +689,32 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Du erhältst %(currency)s %(amount)s zurück"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte trage eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Deine lokale Zeit:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-03 10:43+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"
@@ -578,54 +578,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -663,32 +663,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -633,18 +633,18 @@ msgstr "Δημιουργία μηνυμάτων …"
msgid "Unknown error."
msgstr "Άγνωστο σφάλμα."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
"Το χρώμα σας έχει μεγάλη αντίθεση και είναι πολύ εύκολο να το διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Το χρώμα σας έχει αξιοπρεπή αντίθεση και είναι ίσως αρκετά καλό για να "
"διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -652,40 +652,40 @@ msgstr ""
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
"σκούρα σκιά."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Όλα"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -729,40 +729,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για ένα λεπτό."
msgstr[1] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για {num} λεπτά."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "απο %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "απο %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+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"
@@ -578,54 +578,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -663,32 +663,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2023-11-07 14:00+0000\n"
"Last-Translator: Zona Vip <contacto@zonavip.mx>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -604,17 +604,17 @@ msgstr "Generando mensajes…"
msgid "Unknown error."
msgstr "Error desconocido."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "¡Tu color tiene gran contraste y es muy fácil de leer!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"¡Tu color tiene un contraste decente y es probablemente lo suficientemente "
"legible!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -622,40 +622,40 @@ msgstr ""
"Tu color tiene mal contraste para un texto con fondo blanco, por favor, "
"escoge un tono más oscuro."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Consultar búsqueda"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Solamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "¡Tienes cambios sin guardar!"
@@ -697,32 +697,32 @@ msgstr[0] ""
msgstr[1] ""
"Los elementos en su carrito de compras se han reservado por {num} minutos."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "El organizador se queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Obtienes %(currency)s %(price)s de vuelta"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "campo requerido"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Su hora local:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -603,54 +603,54 @@ msgstr ""
msgid "Unknown error."
msgstr "Tuntematon virhe."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Kaikki"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Käytä toista nimeä sisäisesti"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Sulje klikkaamalla"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Sinulla on tallentamattomia muutoksia!"
@@ -692,34 +692,34 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
msgstr[1] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Ostoskori on vanhentunut"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Aikavyöhyke:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2023-09-11 10:00+0000\n"
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -604,16 +604,16 @@ msgstr "Création de messages …"
msgid "Unknown error."
msgstr "Erreur inconnue."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "Votre choix couleur a un bon contraste et il est très facile à lire!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Votre choix de couleur est assez bon pour la lecture et a un bon contraste !"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -621,40 +621,40 @@ msgstr ""
"Votre choix de couleur n'a pas un bon contraste avec du texte sur un fond "
"blanc, SVP choisissez un ton plus sombre."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Requête de recherche"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Tous"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Aucun"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Seuls les sélectionnés"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Utiliser un nom différent en interne"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Cliquez pour fermer"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Vous avez des modifications non sauvegardées !"
@@ -694,33 +694,33 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Les articles de votre panier sont réservés pour une minute."
msgstr[1] "Les articles de votre panier sont réservés pendant {num} minutes."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "L'organisateur retient %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Vous recevez en retour %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
"Veuillez indiquer le montant que l'organisateur est autorisé à retenir."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "SVP entrez une quantité pour un de vos types de billets."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "obligatoire"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Fuseau horaire :"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "Votre heure locale:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -606,17 +606,17 @@ msgstr "Xerando mensaxes…"
msgid "Unknown error."
msgstr "Erro descoñecido."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "A túa cor ten moito contraste e é moi doada de ler!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"A túa cor ten un contraste axeitado e probablemente sexa suficientemente "
"lexible!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -624,40 +624,40 @@ msgstr ""
"A túa cor ten mal contraste para un texto con fondo branco. Por favor, "
"escolle un ton máis escuro."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr "Consultar unha procura"
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Ningún"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr "Soamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Usar un nome diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Tes cambios sen gardar!"
@@ -698,32 +698,32 @@ msgstr[0] "Os artigos da túa cesta están reservados para ti durante un minuto.
msgstr[1] ""
"Os artigos da túa cesta están reservados para ti durante {num} minutos."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "O organizador queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr "Obtés %(currency)s %(price)s de volta"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese a cantidade que pode conservar o organizador."
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduza un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr "campo requirido"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr "A súa hora local:"
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -586,54 +586,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -675,32 +675,32 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2024-01-31 04:00+0000\n"
"Last-Translator: Pavle Ergović <pavleergovic@gmail.com>\n"
"Language-Team: Croatian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -580,54 +580,54 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr ""
@@ -665,32 +665,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 13:37+0000\n"
"POT-Creation-Date: 2024-04-02 15:53+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -623,16 +623,16 @@ msgstr "Üzenetek generálása…"
msgid "Unknown error."
msgstr "Ismeretlen hiba."
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr "A választott színek remek kontrasztot adnak, és nagyon könnyű olvasni!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"A választott színek kontrasztja elégséges, és valószínűleg jól olvasható!"
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -640,40 +640,40 @@ msgstr ""
"A választott színek kontrasztja elégtelen, kérjük válassz sötétebb "
"árnyalatot."
#: pretix/static/pretixcontrol/js/ui/main.js:473
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Összes"
#: pretix/static/pretixcontrol/js/ui/main.js:492
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr "Semmi"
#: pretix/static/pretixcontrol/js/ui/main.js:496
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:792
#: pretix/static/pretixcontrol/js/ui/main.js:794
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:795
#: pretix/static/pretixcontrol/js/ui/main.js:797
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:953
#: pretix/static/pretixcontrol/js/ui/main.js:955
msgid "Use a different name internally"
msgstr "Használj másik nevet"
#: pretix/static/pretixcontrol/js/ui/main.js:993
#: pretix/static/pretixcontrol/js/ui/main.js:995
msgid "Click to close"
msgstr "Bezárásért kattints"
#: pretix/static/pretixcontrol/js/ui/main.js:1068
#: pretix/static/pretixcontrol/js/ui/main.js:1070
msgid "You have unsaved changes!"
msgstr "Mentetlen változtatások!"
@@ -717,40 +717,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "A kosár tartalma egy percig foglalva van számodra."
msgstr[1] "A kosár tartalma {num} percig foglalva van számodra."
#: pretix/static/pretixpresale/js/ui/main.js:201
#: pretix/static/pretixpresale/js/ui/main.js:203
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "%(currency) %(price)-tól"
#: pretix/static/pretixpresale/js/ui/main.js:209
#: pretix/static/pretixpresale/js/ui/main.js:211
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "%(currency) %(price)-tól"
#: pretix/static/pretixpresale/js/ui/main.js:225
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:442
#: pretix/static/pretixpresale/js/ui/main.js:444
msgid "Please enter a quantity for one of the ticket types."
msgstr "Adjon meg egy mennyiséget az egyik jegytípusból."
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:480
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:581
#: pretix/static/pretixpresale/js/ui/main.js:600
#: pretix/static/pretixpresale/js/ui/main.js:583
#: pretix/static/pretixpresale/js/ui/main.js:602
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:591
#: pretix/static/pretixpresale/js/ui/main.js:593
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff

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