Compare commits

...

148 Commits

Author SHA1 Message Date
Raphael Michel
189c77207f Bump version to 2024.4.0 2024-04-26 13:53:49 +02:00
Raphael Michel
3422003a9c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5607 of 5607 strings)

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

powered by weblate
2024-04-26 13:53:17 +02:00
Raphael Michel
8da38ba99d Translations: Update German
Currently translated at 100.0% (5607 of 5607 strings)

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

powered by weblate
2024-04-26 13:53:17 +02:00
Raphael Michel
fc05208b92 Docs: Add note on Swissbit TSE to Android support policy 2024-04-26 13:36:49 +02:00
Raphael Michel
b163109c56 Docs: ADd undocumented metrics 2024-04-26 12:28:38 +02:00
Raphael Michel
3ba818336e Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-04-26 11:26:25 +02:00
Raphael Michel
8aecf4f98f Voucher import: Validate code not empty 2024-04-26 11:25:49 +02:00
Martin Gross
42f3ca9661 SecHelper: Fix typo in logging statement 2024-04-24 18:13:27 +02:00
Raphael Michel
f7b405b210 Voucher import: Do not allow to keep min/max usages empty 2024-04-24 14:20:54 +02:00
Raphael Michel
11a6390cfc Voucher import: Fix quota name check 2024-04-24 10:28:10 +02:00
dependabot[bot]
239a7746df Update djangorestframework requirement from ==3.14.* to ==3.15.* (#4001)
Updates the requirements on [djangorestframework](https://github.com/encode/django-rest-framework) to permit the latest version.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.14.0...3.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2024-04-23 16:56:02 +02:00
Wikinaut
03701eaa82 Docs: Add explicit user change to installation guide (#3972)
* Update manual_smallscale.rst

(I propose to) 
add

- sudo su pretix ; and
- deactivate

commands to the documentation,n

* Update doc/admin/installation/manual_smallscale.rst

* Update doc/admin/installation/manual_smallscale.rst

* Update doc/admin/installation/manual_smallscale.rst

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2024-04-23 15:18:33 +02:00
dependabot[bot]
356f215d8e Update celery requirement from ==5.3.* to ==5.4.* (#4091)
Updates the requirements on [celery](https://github.com/celery/celery) to permit the latest version.
- [Release notes](https://github.com/celery/celery/releases)
- [Changelog](https://github.com/celery/celery/blob/main/Changelog.rst)
- [Commits](https://github.com/celery/celery/compare/v5.3.0a1...v5.4.0)

---
updated-dependencies:
- dependency-name: celery
  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-23 15:11:40 +02:00
dependabot[bot]
7962c4e380 Update django-otp requirement from ==1.4.* to ==1.5.* (#4083)
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.4.0...v1.5.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-23 15:11:31 +02:00
dependabot[bot]
a59711ed32 Update django-statici18n requirement from ==2.4.* to ==2.5.* (#4106)
Updates the requirements on [django-statici18n](https://github.com/zyegfryed/django-statici18n) to permit the latest version.
- [Changelog](https://github.com/zyegfryed/django-statici18n/blob/main/docs/changelog.rst)
- [Commits](https://github.com/zyegfryed/django-statici18n/compare/v2.4.0...v2.5.0)

---
updated-dependencies:
- dependency-name: django-statici18n
  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-23 15:11:18 +02:00
dependabot[bot]
49370a5e08 Update reportlab requirement from ==4.1.* to ==4.2.* (#4092)
Updates the requirements on [reportlab](https://www.reportlab.com/) to permit the latest version.

---
updated-dependencies:
- dependency-name: reportlab
  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-23 15:03:54 +02:00
dependabot[bot]
980aec7326 Update fakeredis requirement from ==2.21.* to ==2.22.* (#4105)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.21.0...v2.22.0)

---
updated-dependencies:
- dependency-name: fakeredis
  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-23 15:03:09 +02:00
Raphael Michel
44294110fe Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5606 of 5606 strings)

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

powered by weblate
2024-04-23 14:59:03 +02:00
Raphael Michel
ce1078a783 Translations: Update German
Currently translated at 100.0% (5606 of 5606 strings)

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

powered by weblate
2024-04-23 14:59:03 +02:00
Raphael Michel
0e0cede0ee Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-04-23 14:41:20 +02:00
Martin Gross
5c833cd493 Add special input widgets for markdown fields (#1577)
* Add markdown text input widgets

* Update src/pretix/static/pretixcontrol/scss/_forms.scss

* Improvements

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-04-23 14:40:30 +02:00
Raphael Michel
64d6a34039 Ensure correct language of invoice attachment file name (Z#23150936) (#4094) 2024-04-23 14:07:30 +02:00
Mira
cf380069b4 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-23 14:07:17 +02:00
Mira
48168a4c68 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-23 14:07:17 +02:00
Raphael Michel
6482fe79b0 Add force to log entry in extend_order() 2024-04-22 21:34:46 +02:00
Raphael Michel
0f696f42f6 Do not allow currency codes that to not represent money (#4056)
* Do not allow currency codes that to not represent money

* Rebase migration

* Fix blacklist
2024-04-22 16:22:30 +02:00
Martin Gross
79d59553d7 POS: Do not fail when rendering ZVT-transactions without a cardName or cardType 2024-04-22 13:52:18 +02:00
Raphael Michel
cc903c39f0 Docs: Fix copy-paste error in COEP docs 2024-04-22 13:18:34 +02:00
Raphael Michel
b6a42ac8d2 Widget: Document and improve situation around COEP (Z#23149909) (#4051) 2024-04-22 13:15:33 +02:00
Raphael Michel
5f5001edb5 File upload: Fix thumbnailing of current image (Z#23151293) (#4098) 2024-04-22 13:14:28 +02:00
Raphael Michel
fb403dad88 Add comment field to subevents (Z#23148472) (#4099)
* Add comment field to subevents (Z#23148472)

* Review fix
2024-04-22 12:56:29 +02:00
Christiaan de Die le Clercq
a73c8f580d Translations: Update Dutch
Currently translated at 88.1% (4937 of 5598 strings)

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

powered by weblate
2024-04-22 11:38:27 +02:00
Raphael Michel
f490c89e98 Seating plans: Do not allow GUIDs starting or ending with spaces (#4093) 2024-04-22 11:31:34 +02:00
Raphael Michel
159658ae46 Fix typo in attribute name 2024-04-19 17:34:38 +02:00
Christiaan de Die le Clercq
595aff0579 Add a community install guide page (#4096) 2024-04-19 15:08:47 +02:00
Raphael Michel
b2842ec3a0 Checkin: Allow to use presence state in rules (#4061) 2024-04-18 13:15:31 +02:00
Raphael Michel
f09f07ec7c Add Android Version Support Policy (#4074) 2024-04-18 11:35:28 +02:00
Richard Schreiber
cff073f0d6 Fix: show human-readable country name in profile description for custom country-code questions (#4078) 2024-04-18 11:08:21 +02:00
dependabot[bot]
eb9d0c6cf9 Update sphinx requirement from ==7.2.* to ==7.3.* (#4088)
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.2.0...v7.3.5)

---
updated-dependencies:
- dependency-name: sphinx
  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-18 11:06:52 +02:00
dependabot[bot]
e263946c3f Update django-bootstrap3 requirement from ==23.6.* to ==24.2 (#4087)
Updates the requirements on [django-bootstrap3](https://github.com/zostera/django-bootstrap3) to permit the latest version.
- [Changelog](https://github.com/zostera/django-bootstrap3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zostera/django-bootstrap3/compare/v23.6...v24.2)

---
updated-dependencies:
- dependency-name: django-bootstrap3
  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-18 11:06:46 +02:00
Shiiko
7ee957cff0 Translations: Update Chinese (Simplified)
Currently translated at 52.5% (2940 of 5598 strings)

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

powered by weblate
2024-04-17 17:18:31 +02:00
Raphael Michel
ac2fe4b62d Fix crash in placeholder samples (PRETIXEU-9YP) 2024-04-17 16:37:40 +02:00
Raphael Michel
577e276df3 Fix voucher import validation issue (PRETIXEU-9YF) 2024-04-17 15:42:05 +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 Michel
009e3a6d36 Respect Order.valid_if_pending for mail_attach_ical_paid_only (#4065) 2024-04-12 15:00:00 +02:00
Martin Gross
c01855270b Approval Process: Attach ical to approval-messages if properly configured (#4064) 2024-04-12 11:18:21 +02:00
dependabot[bot]
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]
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 Gross
58add74b3a Stripe: Add PayPal (#4049) (Z#23123667) 2024-04-09 10:06:58 +02:00
Raphael Michel
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 Michel
a7735d5d9e API: Allow request_valid_from in the past (#4048) 2024-04-03 17:21:25 +02:00
Raphael 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 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 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 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 Michel
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äfer
48493c517b Add database.disable_server_side_cursors option (#4016) 2024-04-03 10:16:48 +02:00
dependabot[bot]
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]
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
Raphael Michel
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
Raphael Michel
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 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 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 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 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 Michel
440c97061c Fix duplicate key on SQLite (#4038)
* Fix duplicate key on SQLite

* Update migration
2024-04-02 17:37:37 +02:00
Raphael Michel
3b6d0c4341 Translations: Add Slovak 2024-04-02 17:28:11 +02:00
Raphael Michel
06ac4b0250 Translations: Add Slovak 2024-04-02 17:28:11 +02:00
Raphael Michel
a233b92f6f Add disable date of waiting list to event timeline (#4036) 2024-04-02 17:15:41 +02:00
Raphael Michel
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
Mira
50838b9cea Update github action versions (#4033) 2024-04-02 13:53:09 +02:00
Raphael Michel
c68ee56d51 Log discarding a valid session for suspicious reasons (#4025) 2024-04-02 13:52:30 +02:00
Thatthep
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]
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
Raphael Michel
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]
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]
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]
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]
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]
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]
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]
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
Raphael Michel
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
Raphael Michel
40c76dda74 Bump version to 2024.3.0 2024-03-27 12:14:07 +01:00
Raphael Michel
f532853021 Memberships: Prefer valid_from over event date for .is_valid() (#4003)
* Memberships: Prefer valid_from over event date for .is_valid()

* Fix tests

* Add parameter description

* Use reasonable default for requested_valid_from if membership starts in the future

* Set datetimepicker viewDate to closest allowed date

* Keep current value on going back to QuestionsStep

* Fix min_date/max_date in SplitDateTimePickerWidget

* Remove unused import

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

Co-authored-by: Mira <weller@rami.io>

* Respect variations

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-03-27 12:11:20 +01:00
Martin Gross
8cb187502d Event Fonts: Only run register_event_fonts with actual, still existing events (FIXES PRETIXEU-9WE) 2024-03-26 17:13:13 +01:00
Raphael Michel
156037f2cd Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5574 of 5574 strings)

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

powered by weblate
2024-03-26 16:53:43 +01:00
Raphael Michel
134d63fb3f Translations: Update German
Currently translated at 100.0% (5574 of 5574 strings)

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

powered by weblate
2024-03-26 16:53:43 +01:00
Raphael Michel
816002fda0 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5572 of 5574 strings)

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

powered by weblate
2024-03-26 16:53:43 +01:00
Raphael Michel
3939bbc11c Translations: Update German
Currently translated at 100.0% (5574 of 5574 strings)

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

powered by weblate
2024-03-26 16:53:43 +01:00
Raphael Michel
95d1603cc7 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-03-26 14:37:18 +01:00
Raphael Michel
ada3ada699 Fix #3974 -- Webhooks: Add type of checkin (#3994) 2024-03-26 14:21:57 +01:00
Richard Schreiber
97eaeac4f2 Fix: improve order-change headline on customer’s order page (Z#23148804) (#4012) 2024-03-26 11:19:17 +01:00
Martin Gross
d67f5c650c Event-specific fonts and Web-Embedded Fonts (Z#23130701) (#3893) 2024-03-26 09:55:08 +01:00
Raphael Michel
273c1ae0a6 Waiting list: Allow to set auto-disable date (Z#23141338) (#4004)
* Waiting list: Allow to set auto-disable date (Z#23141338)

* ADd warning on non-esries events
2024-03-22 11:17:02 +01:00
Mira
a946c10ab4 Build jsi18n for all supported languages (#4007) 2024-03-22 10:19:27 +01:00
Raphael Michel
2d8fba7d7c Treat partially paid expired orders as overpaid orders (Z#23147757) (#3990)
* Treat partially paid expired orders as overpaid orders (Z#23147757)

* Use is_overpaid from annotate_overpayments in OrderFilterForm

* Revert change to pending sum

* Show warning on order page

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-03-22 10:17:51 +01:00
Mira
e4e0bd7ca0 Fix regression in thumb filter (#4006) 2024-03-21 14:32:01 +01:00
Richard Schreiber
3651c88289 Widget: pass utm-params from embedding page to presale 2024-03-20 09:19:02 +01:00
Richard Schreiber
c92ca40026 Fix code style/flake8 issue 2024-03-19 13:56:43 +01:00
Mira
4d00efb549 Fix generating thumbs for favicon.ico 2024-03-19 13:54:02 +01:00
Raphael Michel
7e60d13910 Fix #3984 -- API: Add phone to customer resource (#3992)
* Fix #3984 -- API: Add phone to customer resource

* add "phone": None to test

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-03-19 10:17:44 +01:00
Martin Weinelt
35800e21c7 Allow customization of cache and log directory (#3997)
On systems that follow the FHS it may be desirable to separate logs and
cache files into dedicated base directories (e.g. /var/log/pretix or
/var/cache/pretix).
2024-03-19 10:17:36 +01:00
Raphael Michel
99b4c5bd36 Fix cart grouping function to include membership and validity (#3991)
* Fix cart grouping function to include membership and validity

* Update src/pretix/presale/views/__init__.py

Co-authored-by: Mira <weller@rami.io>

---------

Co-authored-by: Mira <weller@rami.io>
2024-03-19 10:17:26 +01:00
Raphael Michel
f121205dd1 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (225 of 225 strings)

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

powered by weblate
2024-03-19 10:16:58 +01:00
Raphael Michel
1ac54cd209 Translations: Update German
Currently translated at 100.0% (225 of 225 strings)

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

powered by weblate
2024-03-19 10:16:58 +01:00
Raphael Michel
4694719a53 API: Fix creating free orders requiring approval 2024-03-19 10:10:57 +01:00
Raphael Michel
9513b6e8d7 Gift card payment: Fix public_name fallback 2024-03-15 17:09:21 +01:00
dependabot[bot]
4fd7d406a0 Update sentry-sdk requirement from ==1.41.* to ==1.42.* (#3983)
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.41.0...1.42.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-03-15 16:49:52 +01:00
dependabot[bot]
47cb5b207a Update pytest-rerunfailures requirement from ==13.* to ==14.* (#3982)
Updates the requirements on [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/13.0...14.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  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-03-15 16:42:03 +01:00
dependabot[bot]
7d2cf68727 Bump markdown from 3.5.2 to 3.6 (#3985)
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.5.2 to 3.6.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.5.2...3.6)

---
updated-dependencies:
- dependency-name: markdown
  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-03-15 16:41:49 +01:00
dependabot[bot]
459cb47ca8 Update protobuf requirement from ==4.25.* to ==5.26.* (#3986)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v4.25.0-rc1...v5.26.0)

---
updated-dependencies:
- dependency-name: protobuf
  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-03-15 16:41:31 +01:00
Mira
39705556cd Fix tests that failed between 0:00 and 4:00 / 4:30 AM (#3987) 2024-03-15 16:40:56 +01:00
Raphael Michel
9f794290dc Memberships: Check valid_from/valid_until for parallel usage (#3975) 2024-03-15 16:40:41 +01:00
Raphael Michel
b6221ab6d9 Improve error messages for test mode checks for memberships 2024-03-15 15:57:11 +01:00
Richard Schreiber
483518bce9 Fix: left align first column header on invoices without tax 2024-03-15 13:34:19 +01:00
Raphael Michel
d9019ae735 Fix splitting free orders that require approval 2024-03-15 11:37:27 +01:00
Raphael Michel
721fd3b998 Remove incorrectly named, duplicate test file 2024-03-15 10:46:47 +01:00
Raphael Michel
ad0d3f5469 PDF editor: Minor UX adjustments 2024-03-14 12:34:21 +01:00
Raphael Michel
40b44f9272 Stripe: Do not open 3rd-party payment pages in iframe (#3981)
* Stripe: Do not open 3rd-party payment pages in iframe

* Update src/pretix/plugins/stripe/payment.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-03-14 11:49:41 +01:00
Mira
304d290f22 Presale: improve clientside handling of max-count for add-on products
* Fix typo in error message

* Use exclusive checkboxes for addon items with max_count == 1 and !multi_allowed

* combine exclusive items + variations

* move exclusive to containing fieldset

* fix add-on-exclusive

* add max_count check

* fix plus/minus-stepper buttons bubbling

* Update src/pretix/static/pretixpresale/js/ui/main.js

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-03-14 09:17:42 +01:00
Richard Schreiber
7592a8a575 Presale: add data-attribute price to each item’s article-element
* Presale: add data-attribute price to article

* fix price for items (defaul_price)

Co-authored-by: Mira <weller@rami.io>

* add data-price to voucher and addons, unlocalize price

* use universal display_price for items and variation, respect net or gross

---------

Co-authored-by: Mira <weller@rami.io>
2024-03-14 09:15:44 +01:00
dependabot[bot]
4f33159f93 Update sentry-sdk requirement from ==1.40.* to ==1.41.* (#3963)
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.40.0...1.41.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-03-12 13:05:09 +01:00
dependabot[bot]
819ce6abf7 Bump django-filter from 23.5 to 24.1 (#3969)
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 23.5 to 24.1.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/23.5...24.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 13:04:54 +01:00
dependabot[bot]
760dfd22b8 Bump @babel/core from 7.23.9 to 7.24.0 in /src/pretix/static/npm_dir (#3951)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.9 to 7.24.0.
- [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.0/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-03-12 13:04:31 +01:00
Mira
f9eaa193c9 Add migration to force slash after hostname in returnurl prefixes (#3948)
* Add migration to force slash after hostname in returnurl prefixes

* isort

* add dependency to pretixbase
2024-03-12 12:41:48 +01:00
dependabot[bot]
c7720a2553 Update pytest requirement from ==8.0.* to ==8.1.* (#3976)
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0rc1...8.1.1)

---
updated-dependencies:
- dependency-name: pytest
  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-03-12 12:41:33 +01:00
fyksen
7754f5420c Translations: Update Norwegian Bokmål
Currently translated at 98.1% (5462 of 5565 strings)

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

powered by weblate
2024-03-12 12:41:21 +01:00
Raphael Michel
2c7ada6e86 OIDC: Do not expect uid to be string 2024-03-12 10:52:20 +01:00
dependabot[bot]
3f31843fd1 Update dnspython requirement from ==2.5.* to ==2.6.* (#3927)
Updates the requirements on [dnspython](https://github.com/rthalley/dnspython) to permit the latest version.
- [Release notes](https://github.com/rthalley/dnspython/releases)
- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)
- [Commits](https://github.com/rthalley/dnspython/compare/v2.5.0rc1...v2.6.1)

---
updated-dependencies:
- dependency-name: dnspython
  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-03-11 13:19:11 +01:00
fyksen
952b9bd9b9 Translations: Update Norwegian Bokmål
Currently translated at 95.5% (215 of 225 strings)

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

powered by weblate
2024-03-11 13:18:45 +01:00
fyksen
3619a6bcd0 Translations: Update Norwegian Bokmål
Currently translated at 98.1% (5462 of 5565 strings)

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

powered by weblate
2024-03-11 13:18:45 +01:00
Richard Schreiber
3e2c12cdb0 Presale: fix auto-unchecked button-checkboxes (#3968) 2024-03-08 19:33:06 +01:00
275 changed files with 163899 additions and 114611 deletions

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') }}

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') }}

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') }}

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

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') }}

View File

@@ -52,10 +52,18 @@ Example::
``currency``
The default currency as a three-letter code. Defaults to ``EUR``.
``cachedir``
The local path to a directory where temporary files will be stored.
Defaults to the ``cache`` directory below the ``datadir``.
``datadir``
The local path to a data directory that will be used for storing user uploads and similar
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
``logdir``
The local path to a directory where log files will be stored.
Defaults to the ``logs`` directory below the ``datadir``.
``plugins_default``
A comma-separated list of plugins that are enabled by default for all new events.
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
@@ -89,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
@@ -149,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
@@ -169,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.

View File

@@ -0,0 +1,13 @@
.. highlight:: none
.. _`community`:
Community install guides
================================
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
Kubernetes
------------
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.

View File

@@ -14,3 +14,4 @@ for your needs.
manual_smallscale
dev_version
enterprise
community

View File

@@ -120,6 +120,7 @@ Now we will install pretix itself. The following steps are to be executed as the
actually install pretix, we will create a virtual environment to isolate the python packages from your global
python installation::
# sudo -u pretix -s
$ python3 -m venv /var/pretix/venv
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
@@ -279,6 +280,7 @@ Updates
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
(venv)$ python -m pretix migrate

View File

@@ -103,6 +103,12 @@ pretix_celery_tasks_queued_count
pretix_celery_tasks_queued_age_seconds
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
pretix_successful_logins
Counter. The number of successful backend logins.
pretix_failed_logins
Counter. The number of failed backend logins, labeled with ``reason``.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/
.. _cProfile: https://docs.python.org/3/library/profile.html

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
}

View File

@@ -19,6 +19,7 @@ external_identifier string External ID of
the API, but is read-only for customers created through a
SSO integration.
email string Customer email address
phone string Customer phone number
name string Name of this customer (or ``null``)
name_parts object of strings Decomposition of name (i.e. given name, family name)
is_active boolean Whether this account is active
@@ -39,6 +40,10 @@ password string Can only be set
Passwords can now be set through the API during customer creation.
.. versionchanged:: 2024.3
The attribute ``phone`` has been added.
Endpoints
---------
@@ -71,6 +76,7 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -118,6 +124,7 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -155,6 +162,7 @@ Endpoints
{
"email": "test@example.org",
"phone": "+493012345678",
"password": "verysecret",
"send_email": true
}
@@ -171,6 +179,7 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
...
}
@@ -215,6 +224,7 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
}
@@ -249,6 +259,7 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": null,
"phone": null,
}

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,
...
}

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``.

View File

@@ -84,6 +84,8 @@ convenient to you:
.. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page

View File

@@ -1,4 +1,4 @@
sphinx==7.2.*
sphinx==7.3.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -1,5 +1,5 @@
-e ../
sphinx==7.2.*
sphinx==7.3.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -0,0 +1,113 @@
Android version support policy
==============================
Building software for Android always presents a struggle between keeping compatibility with older hardware to save cost
and utilizing feature of new Android versions to improve functionality, security and stability. To help you plan ahead,
we are publishing our intended schedule. This is to be understood as a minimum commitment, we will only drop support for
older versions if there is a technical reason to do so, not because the scheduled time has been reached.
.. warning:: This is a non-binding document. We will try our very best to not to deprecate support for Android versions
earlier than listed here, but for technical or economical reasons, it might become necessary to do so under
specific circumstances. Specifically, we might be forced to partially drop support for Android versions
earlier where we integrate third-party components into our software. Typical examples would be specific
payment terminal or printer types where we use a third-party component provided by the hardware vendor.
If we no longer support an Android version, it means that we will no longer publish new versions of the app supporting
that Android version. This means you are not getting new features or bug fixes, and at some point your app might stop
working with the pretix server.
pretixSCAN
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPOS
---------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 | Support planned until at least 12/2029.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 13 | Support planned until at least 12/2028.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 12 | Support planned until at least 12/2027.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 11 | Support planned until at least 12/2026.
| No support for Swissbit microSD TSE.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 | Support planned until at least 12/2025.
| Support for Stripe Terminal on some devices to be dropped 05/2024.
Android 7 | Support planned until at least 12/2024.
| Support for Stripe Terminal to be dropped 05/2024.
| No support for Cryptovision TSE.
Android 6 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
Android 5 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
| No support for SumUp.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPRINT
-----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for Evolis printers on some devices.
Android 4.4 | Support planned until at least 06/2024.
| No support for USB printers.
| No support for Evolis printers.
Android 4 Support dropped.
=========================== ==========================================================
pretixLEAD
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 12/2024.
Android 6 Support planned until at least 12/2024.
Android 5 Support planned until at least 12/2024.
Android 4 Support dropped.
=========================== ==========================================================

View File

@@ -17,8 +17,8 @@ and then click "Generate widget code".
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css" crossorigin>
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async crossorigin></script>
The second snippet should be embedded at the position where the widget should show up::
@@ -449,5 +449,14 @@ Further reading:
* `Stripe Payment Method Domain registration`_
Working with Cross-Origin-Embedder-Policy
-----------------------------------------
The pretix widget is unfortunately not compatible with ``Cross-Origin-Embedder-Policy: require-corp``. If you include
the ``crossorigin`` attributes on the ``<script>`` and ``<link>`` tag as shown above, the widget can show a calendar
or product list, but will not be able to open the checkout process in an iframe. If you also set
``Cross-Origin-Opener-Policy: same-origin``, the widget can auto-detect that it is running in an isolated enviroment
and will instead open the checkout process in a new tab.
.. _Let's Encrypt: https://letsencrypt.org/
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration

View File

@@ -16,4 +16,5 @@ wanting to use pretix to sell tickets.
events/giftcards
faq
markdown
android-version-support
glossary

View File

@@ -11,6 +11,9 @@ In many places of your shop, like frontpage texts, product descriptions and emai
since it is way easier to learn than languages like HTML but allows all basic formatting options required
for text in those places.
.. note:: Some fields that are used in one-line context only allow formatting that refers to individual words
(such as bold or italic font or a link) but do not allow block-level formatting like lists or headlines.
Formatting rules
----------------

View File

@@ -30,17 +30,17 @@ dependencies = [
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.3.*",
"celery==5.4.*",
"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-bootstrap3==24.2",
"django-compressor==4.4",
"django-countries==7.5.*",
"django-filter==23.5",
"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,13 +50,13 @@ dependencies = [
"django-localflavor==4.0",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.3.*",
"django-otp==1.5.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.4.*",
"djangorestframework==3.14.*",
"dnspython==2.5.*",
"django-statici18n==2.5.*",
"djangorestframework==3.15.*",
"dnspython==2.6.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
@@ -65,7 +65,7 @@ dependencies = [
"kombu==5.3.*",
"libsass==0.23.*",
"lxml",
"markdown==3.5.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.2.*",
@@ -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==4.25.*",
"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
@@ -90,9 +90,9 @@ dependencies = [
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"reportlab==4.1.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==1.40.*",
"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.*"
]
@@ -112,7 +113,7 @@ dev = [
"aiohttp==3.9.*",
"coverage",
"coveralls",
"fakeredis==2.21.*",
"fakeredis==2.22.*",
"flake8==7.0.*",
"freezegun",
"isort==5.13.*",
@@ -122,11 +123,11 @@ dev = [
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.12.*",
"pytest-rerunfailures==13.*",
"pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar",
"pytest-xdist==3.5.*",
"pytest==8.0.*",
"pytest==8.1.*",
"responses",
]

4
src/.watchmanconfig Normal file
View File

@@ -0,0 +1,4 @@
{
"ignore_dirs": ["node_modules", "data", "pretix/static", "pretix/locale", "pretix/static.dist"]
}

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

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.dev0"
__version__ = "2024.4.0"

View File

@@ -111,6 +111,7 @@ LANGUAGES_RTL = {
LANGUAGES_INCUBATING = {
'fi', 'pt-br', 'gl',
}
LANGUAGES = ALL_LANGUAGES
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
]
@@ -234,7 +235,12 @@ COMPRESS_FILTERS = {
)
}
CURRENCIES = list(currencies)
CURRENCIES = [
c for c in currencies
if c.alpha_3 not in {
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
]
CURRENCY_PLACES = {
# default is 2
'BIF': 0,

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

View File

@@ -472,7 +472,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state',
'comment')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -687,6 +688,7 @@ class EventSettingsSerializer(SettingsSerializer):
'allow_modifications_after_checkin',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',

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)
@@ -1585,7 +1586,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
@@ -1597,6 +1601,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
if validated_data.get('require_approval'):
raise ValidationError('You cannot create a paid order that requires approval.')
order.payments.create(
amount=order.total,
provider=payment_provider,

View File

@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
class Meta:
model = Customer
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
'locale', 'last_modified', 'notes')
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
def update(self, instance, validated_data):
if instance and instance.provider_id:
@@ -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'

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:

View File

@@ -176,7 +176,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
@@ -185,6 +185,7 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['type'] = logentry.parsed_data.get('type')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
return d
@@ -296,11 +297,11 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.denied',
_('Order denied'),
),
ParametrizedOrderPositionWebhookEvent(
ParametrizedOrderPositionCheckinWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
),
ParametrizedOrderPositionWebhookEvent(
ParametrizedOrderPositionCheckinWebhookEvent(
'pretix.event.checkin.reverted',
_('Ticket check-in reverted'),
),

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

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:

View File

@@ -39,6 +39,7 @@ from django import forms
from django.core.validators import URLValidator
from django.forms.models import ModelFormMetaclass
from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
@@ -85,6 +86,43 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
class MarkdownTextarea(forms.Textarea):
def _render(self, template_name, context, renderer=None):
return mark_safe(
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
super()._render(template_name, context, renderer=None),
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
)
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
SECRET_REDACTED = '*****'

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',

View File

@@ -609,27 +609,38 @@ class BaseQuestionsForm(forms.Form):
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
else:
max_date = None
min_date = now()
initial = None
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
if pos.used_membership.date_start >= now():
initial = min_date = pos.used_membership.date_start
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
attrs = {}
if max_date:
attrs['data-max'] = max_date.date().isoformat()
if min_date:
attrs['data-min'] = min_date.date().isoformat()
self.fields['requested_valid_from'] = forms.DateField(
label=_('Start date'),
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
widget=DatePickerWidget(attrs),
validators=[MaxDateValidator(max_date.date())] if max_date else []
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
)
else:
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
label=_('Start date'),
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
widget=SplitDateTimePickerWidget(
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=min_date,
max_date=max_date
),
validators=[MaxDateTimeValidator(max_date)] if max_date else []
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
)
add_fields = {}

View File

@@ -33,7 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import os
from datetime import date
from datetime import datetime
from django import forms
from django.utils.formats import get_format
@@ -188,11 +188,11 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs['autocomplete'] = 'off'
if min_date:
date_attrs['data-min'] = (
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
min_date if not isinstance(min_date, datetime) else min_date.astimezone(get_current_timezone()).date()
).isoformat()
if max_date:
date_attrs['data-max'] = (
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
).isoformat()
def date_placeholder():

View File

@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items():
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
@@ -625,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
)]
else:
tdata = [(
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]

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"])

View File

@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from urllib.parse import urlsplit
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
@@ -40,6 +40,7 @@ from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
from pretix.presale.style import get_fonts
_supported = None
@@ -240,6 +241,14 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
@@ -259,6 +268,14 @@ class SecurityMiddleware(MiddlewareMixin):
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
'script-src': ['{static}'],
@@ -267,7 +284,7 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"],
'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or

View File

@@ -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",
),
),
]

View File

@@ -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),
),
]

View File

@@ -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(),
),
]

View File

@@ -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,
),
),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-04-19 14:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0261_userknownloginsource"),
]
operations = [
migrations.AddField(
model_name="subevent",
name="comment",
field=models.TextField(null=True),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.2.10 on 2024-04-09 07:32
from django.db import migrations
def change_currencies(apps, schema_editor):
Event = apps.get_model("pretixbase", "Event")
Event.objects.filter(
currency__in={
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
).update(currency='XXX')
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0262_subevent_comment"),
]
operations = [
migrations.RunPython(
change_currencies, migrations.RunPython.noop
)
]

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]

View File

@@ -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))

View File

@@ -0,0 +1,387 @@
#
# 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 not value:
raise ValidationError(_('A voucher cannot be created without a code.'))
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')
default_value = None
initial = "static:1"
def static_choices(self):
return [
("1", "1")
]
def clean(self, value, previous_values):
if value is None:
raise ValidationError(_('The maximum number of usages must be set.'))
return super().clean(value, previous_values)
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')
default_value = None
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 q.name == value
]
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

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)

View File

@@ -285,7 +285,7 @@ class CheckinList(LoggedModel):
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate', 'entry_status',
}
if not rules or not isinstance(rules, dict):
return rules

View File

@@ -229,6 +229,14 @@ class EventMixin:
else:
return self.presale_end
@property
def waiting_list_active(self):
if not self.settings.waiting_list_enabled:
return False
if self.settings.waiting_list_auto_disable:
return self.settings.waiting_list_auto_disable.datetime(self) > now()
return True
@property
def presale_has_ended(self):
"""
@@ -1229,6 +1237,9 @@ class Event(EventMixin, LoggedModel):
if self.has_paid_things and not self.has_payment_provider:
issues.append(_('You have configured at least one paid product but have not enabled any payment methods.'))
if self.has_paid_things and self.currency == "XXX":
issues.append(_('You have configured at least one paid product but have not configured a currency.'))
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
@@ -1450,13 +1461,17 @@ class SubEvent(EventMixin, LoggedModel):
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")
verbose_name=_("Frontpage text"),
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents', verbose_name=_('Seating plan'))
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
comment = models.TextField(
verbose_name=_("Internal comment"),
null=True, blank=True
)
last_modified = models.DateTimeField(
auto_now=True, db_index=True

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"

View File

@@ -49,7 +49,8 @@ class MembershipType(LoggedModel):
allow_parallel_usage = models.BooleanField(
verbose_name=_('Parallel usage is allowed'),
help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note '
'that this will only check for an identical start time of the events, not for any overlap between events.'),
'that this will only check for an identical start time of the events, not for any overlap between events. An overlap '
'check will be performed if there is a product-level validity of the ticket.'),
default=False
)
max_usages = models.PositiveIntegerField(
@@ -162,8 +163,12 @@ class Membership(models.Model):
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
def is_valid(self, ev=None):
if ev:
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen:
return not self.canceled and self.date_end >= now()
elif ticket_valid_from:
dt = ticket_valid_from
elif ev:
dt = ev.date_from
else:
dt = now()

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
@@ -451,9 +462,9 @@ class Order(LockModel, LoggedModel):
if results:
qs = qs.annotate(
is_overpaid=Case(
When(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=-1e-8),
When(~Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_t__lt=-1e-8),
then=Value(1)),
When(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=-1e-8),
When(Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_rc__lt=-1e-8),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
@@ -468,7 +479,7 @@ class Order(LockModel, LoggedModel):
is_underpaid=Case(
When(Q(status=Order.STATUS_PAID) & Q(pending_sum_t__gt=1e-8),
then=Value(1)),
When(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__gt=1e-8),
When(Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_rc__gt=1e-8),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
@@ -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))),

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,

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):

View File

@@ -57,7 +57,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import PlaceholderValidator
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota, TaxRule,
@@ -1185,14 +1185,14 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description during checkout'),
help_text=_('This text will be shown during checkout when the user selects this payment method. '
'It should give a short explanation on this payment method.'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)),
('email_instructions', I18nFormField(
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
('pending_description', I18nFormField(
@@ -1200,7 +1200,7 @@ class ManualPayment(BasePaymentProvider):
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
('invoice_immediately',
@@ -1311,9 +1311,7 @@ class GiftCardPayment(BasePaymentProvider):
@property
def public_name(self) -> str:
return str(self.settings.get("public_name", as_type=LazyI18nString)) or _(
"Gift card"
)
return str(self.settings.get("public_name", as_type=LazyI18nString) or _("Gift card"))
@property
def settings_form_fields(self):
@@ -1327,7 +1325,7 @@ class GiftCardPayment(BasePaymentProvider):
(
"public_description",
I18nFormField(
label=_("Payment method description"), widget=I18nTextarea, required=False
label=_("Payment method description"), widget=I18nMarkdownTextarea, required=False
),
),
]

View File

@@ -78,7 +78,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -738,9 +738,10 @@ class Renderer:
else:
self.bg_bytes = None
self.bg_pdf = None
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
@classmethod
def _register_fonts(cls):
def _register_fonts(cls, event: Event = None):
if hasattr(cls, '_fonts_registered'):
return
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
@@ -748,7 +749,7 @@ class Renderer:
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
for family, styles in get_fonts().items():
for family, styles in get_fonts(event, pdf_support_required=True).items():
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
@@ -934,6 +935,13 @@ class Renderer:
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
font = o['fontfamily']
# 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.
if font not in self.event_fonts:
logger.warning(f'Unauthorized use of font "{font}"')
font = 'Open Sans'
if o['bold']:
font += ' B'
if o['italic']:

View File

@@ -203,7 +203,7 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'),
@@ -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:

View File

@@ -42,8 +42,8 @@ from dateutil.tz import datetime_exists
from django.core.files import File
from django.db import IntegrityError, transaction
from django.db.models import (
BooleanField, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, Value,
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When,
)
from django.db.models.functions import Coalesce, TruncDate
from django.dispatch import receiver
@@ -273,6 +273,14 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
var_texts[vname] = _('Only allowed before {datetime}').format(datetime=compare_to_text)
elif operator == 'isAfter':
var_texts[vname] = _('Only allowed after {datetime}').format(datetime=compare_to_text)
elif var == 'entry_status':
var_weights[vname] = (20, 0)
if operator == '==' and rhs[0] == 'present':
var_texts[vname] = _('Attendee is checked out')
elif operator == '==' and rhs[0] == 'absent':
var_texts[vname] = _('Attendee is already checked in')
else:
var_texts[vname] = f'{var} not {operator} {rhs}'
elif var == 'product' or var == 'variation':
var_weights[vname] = (1000, 0)
var_texts[vname] = _('Ticket type not allowed')
@@ -507,6 +515,13 @@ class LazyRuleVars:
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
@cached_property
def entry_status(self):
last_checkin = self._position.checkins.filter(list=self._clist).order_by('datetime').last()
if not last_checkin or last_checkin.type == Checkin.TYPE_EXIT:
return "absent"
return "present"
@cached_property
def minutes_since_last_entry(self):
tz = self._clist.event.timezone
@@ -569,6 +584,8 @@ class SQLLogic:
'entries_days_since', 'entries_days_before'}
def operation_to_expression(self, rule):
if isinstance(rule, str):
return Value(rule)
if not isinstance(rule, dict):
return rule
@@ -770,6 +787,25 @@ class SQLLogic:
Value(-1),
output_field=IntegerField()
)
elif values[0] == 'entry_status':
sq_last_checkin = Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.list.pk,
).order_by('-datetime').values('type')[:1]
)
return Case(
When(
condition=Equal(
sq_last_checkin,
Value(Checkin.TYPE_ENTRY)
),
then=Value("present"),
),
default=Value("absent"),
output_field=TextField()
)
else:
raise ValueError(f'Unknown operator {operator}')

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()

View File

@@ -470,7 +470,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
# just "ABC-123.pdf", but we only do so if our currently selected language allows to do this
# as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this
# has shown to cause deliverability problems of the email and deliverability wins.
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
with language(order.locale if order else inv.locale, event.settings.region if event else None):
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename):
filename = inv.number.replace(' ', '_') + '.pdf'
filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename)

View File

@@ -29,8 +29,8 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import (
AbstractPosition, Customer, Event, Item, Membership, Order, OrderPosition,
SubEvent,
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
OrderPosition, SubEvent,
)
from pretix.helpers import OF_SELF
@@ -82,7 +82,8 @@ def create_membership(customer: Customer, position: OrderPosition):
)
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False):
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
valid_from_not_chosen=False):
"""
Validate that a set of cart or order positions. This currently does not validate
@@ -92,6 +93,8 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
:param lock: Whether to place a SELECT FOR UPDATE lock on the selected memberships
:param ignored_order: An order that should be ignored for usage counting
:param testmode: If ``True``, only test mode memberships are allowed. If ``False``, test mode memberships are not allowed.
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
"""
tz = event.timezone
applicable_positions = [
@@ -132,7 +135,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
qs = qs.exclude(order_id=ignored_order.pk)
m._used_at_dates = [
(op.subevent or op.order.event).date_from
for op in qs
for op in qs if not op.valid_from or not op.valid_until
]
m._used_for_ranges = [
(op.valid_from, op.valid_until)
for op in qs if op.valid_from or op.valid_until
]
for p in applicable_positions:
@@ -147,22 +154,44 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
_('You selected membership that has been canceled.')
)
if m.testmode != testmode:
if m.testmode and not testmode:
raise ValidationError(
_('You can only use a test mode membership for test mode tickets.')
_('You can not use a test mode membership for tickets that are not in test mode.')
)
elif not m.testmode and testmode:
raise ValidationError(
_('You need to add a test mode membership to the customer account to use it in test mode.')
)
ev = p.subevent or event
if not m.is_valid(ev):
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected an event '
'taking place at {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
if isinstance(p, (OrderPosition, CartPosition)):
# override_ variants are for usage of fake cart in OrderChangeManager
valid_from = getattr(p, 'override_valid_from', p.valid_from)
valid_until = getattr(p, 'override_valid_until', p.valid_until)
else: # future safety, not technically defined on AbstractPosition
valid_from = None
valid_until = None
if not m.is_valid(ev, valid_from, valid_from_not_chosen=p.item.validity_dynamic_start_choice and valid_from_not_chosen):
if valid_from:
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected a ticket that '
'starts to be valid on {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
else:
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected an event '
'taking place at {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
)
if p.variation and p.variation.require_membership:
types = p.variation.require_membership_types.all()
@@ -188,13 +217,34 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
m.usages += 1
if not m.membership_type.allow_parallel_usage:
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
if (valid_from or valid_until) and not (p.item.validity_dynamic_start_choice and valid_from_not_chosen):
for used_range in m._used_for_ranges:
if valid_from and valid_from > used_range[1]:
continue
if valid_until and valid_until < used_range[0]:
continue
raise ValidationError(
_('You are trying to use a membership of type "{type}" for a ticket valid from {valid_from} '
'until {valid_until}, however you already used the same membership for a different ticket '
'that overlaps with this time frame ({conflict_from} {conflict_until}).').format(
type=m.membership_type.name,
valid_from=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_from else _('start'),
valid_until=date_format(valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_until else _('open end'),
conflict_from=date_format(used_range[0].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[0] else _('start'),
conflict_until=date_format(used_range[1].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[1] else _('open end'),
)
)
)
m._used_at_dates.append(ev.date_from)
m._used_for_ranges.append((p.valid_from, p.valid_until))
if not valid_from or not valid_until:
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
m._used_at_dates.append(ev.date_from)

View File

@@ -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()

View File

@@ -197,8 +197,9 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
'currency_XXX': gettext_lazy('Paid products not supported without a valid currency.'),
}
logger = logging.getLogger(__name__)
@@ -220,7 +221,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
is_available = order._is_still_available(now(), count_waitinglist=False, check_voucher_usage=True,
check_memberships=True, lock=True, force=force)
if is_available is True:
if order.payment_refund_sum >= order.total:
if order.payment_refund_sum >= order.total and not order.require_approval:
order.status = Order.STATUS_PAID
else:
order.status = Order.STATUS_PENDING
@@ -297,6 +298,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
auth=auth,
data={
'expires': order.expires,
'force': force,
'state_change': was_expired
}
)
@@ -412,6 +414,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 +672,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 +883,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 +898,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 +1059,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 +1082,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],
@@ -1109,6 +1131,9 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
id__in=position_ids, event=event
)
if shown_total is not None and Decimal(shown_total) > Decimal("0.00") and event.currency == "XXX":
raise OrderError(error_messages['currency_XXX'])
validate_order.send(
event,
payment_provider=payment_requests[0]['provider'] if payment_requests else None, # only for backwards compatibility
@@ -1144,7 +1169,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):
@@ -2105,6 +2130,9 @@ class OrderChangeManager:
)
def _check_paid_to_free(self):
if self.event.currency == 'XXX' and self.order.total + self._totaldiff > Decimal("0.00"):
raise OrderError(error_messages['currency_XXX'])
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval:
if not self.order.fees.exists() and not self.order.positions.exists():
# The order is completely empty now, so we cancel it.
@@ -2500,7 +2528,7 @@ class OrderChangeManager:
remaining_total = sum([p.price for p in self.order.positions.all()]) + sum([f.value for f in self.order.fees.all()])
offset_amount = min(max(0, self.completed_payment_sum - remaining_total), split_order.total)
if offset_amount >= split_order.total:
if offset_amount >= split_order.total and not split_order.require_approval:
split_order.status = Order.STATUS_PAID
else:
split_order.status = Order.STATUS_PENDING
@@ -2671,6 +2699,7 @@ class OrderChangeManager:
for p in self.order.positions.all():
cp = CartPosition(
event=self.event,
item=p.item,
variation=p.variation,
attendee_name_parts=p.attendee_name_parts,
@@ -2691,16 +2720,23 @@ class OrderChangeManager:
positions_to_fake_cart[op.position].seat = op.seat
elif isinstance(op, self.MembershipOperation):
positions_to_fake_cart[op.position].used_membership = op.membership
elif isinstance(op, self.ChangeValidFromOperation):
positions_to_fake_cart[op.position].override_valid_from = op.valid_from
elif isinstance(op, self.ChangeValidUntilOperation):
positions_to_fake_cart[op.position].override_valid_until = op.valid_until
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
fake_cart.remove(positions_to_fake_cart[op.position])
elif isinstance(op, self.AddOperation):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
try:
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)

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']:

View File

@@ -446,6 +446,11 @@ class QuotaAvailability:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
quotas = [
q for q in quotas
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
]
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas}

View File

@@ -110,6 +110,9 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
continue
if wle.subevent and not wle.subevent.presale_is_running:
continue
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
gone.add((wle.item, wle.variation, wle.subevent))
continue
if not wle.item.is_available():
gone.add((wle.item, wle.variation, wle.subevent))
continue

View File

@@ -64,7 +64,7 @@ from pretix.api.serializers.fields import (
ListMultipleChoiceField, UploadedFileField,
)
from pretix.api.serializers.i18n import I18nField, I18nURLField
from pretix.base.forms import I18nURLFormField
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
@@ -89,7 +89,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts().items() if not v.get('pdf_only', False)
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0])
return {
'choices': choices,
@@ -602,7 +602,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Invoice address explanation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the invoice address form during checkout.")
)
@@ -801,7 +801,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("End of presale text"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
@@ -813,7 +813,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {
'rows': 3,
}},
@@ -1397,6 +1397,19 @@ DEFAULTS = {
widget=forms.NumberInput(),
)
},
'waiting_list_auto_disable': {
'default': None,
'type': RelativeDateWrapper,
'form_class': RelativeDateTimeField,
'serializer_class': SerializerRelativeDateTimeField,
'form_kwargs': dict(
label=_("Disable waiting list"),
help_text=_("The waiting list will be fully disabled after this date. This means that nobody can add "
"themselves to the waiting list any more, but also that tickets will be available for sale "
"again if quota permits, even if there are still people on the waiting list. Vouchers that "
"have already been sent remain active."),
)
},
'waiting_list_names_asked': {
'default': 'False',
'type': bool,
@@ -1446,7 +1459,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Phone number explanation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
@@ -1863,7 +1876,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voluntary lower refund explanation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
"which your customers can use to choose the amount they would like to receive. You can use it "
@@ -1945,7 +1958,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for a paid order. Leave empty if you "
"want pretix to automatically generate the terms of cancellation based on your settings.")
@@ -1958,7 +1971,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for an unpaid or free order. Leave empty "
"if you want pretix to automatically generate the terms of cancellation based on your settings.")
@@ -2047,7 +2060,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Event description"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_(
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
@@ -2978,7 +2991,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Frontpage text"),
widget=I18nTextarea
widget=I18nMarkdownTextarea,
)
},
'event_info_text': {
@@ -3000,7 +3013,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (top)"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above every page of your shop. Please only use this for "
"very important messages.")
@@ -3013,7 +3026,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (bottom)"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown below every page of your shop. Please only use this for "
"very important messages.")
@@ -3026,7 +3039,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voucher explanation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
@@ -3039,7 +3052,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Attendee data explanation"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain "
"why you need information from them.")
@@ -3055,7 +3068,7 @@ Your {organizer} team""")) # noqa: W291
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
"to the default text."),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
widget=I18nMarkdownTextarea,
)
},
'checkout_phone_helptext': {
@@ -3066,7 +3079,7 @@ Your {organizer} team""")) # noqa: W291
'form_kwargs': dict(
label=_("Help text of the phone number field"),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
widget=I18nMarkdownTextarea,
)
},
'checkout_email_helptext': {
@@ -3080,7 +3093,7 @@ Your {organizer} team""")) # noqa: W291
'form_kwargs': dict(
label=_("Help text of the email field"),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
widget=I18nMarkdownTextarea,
)
},
'order_import_settings': {
@@ -3210,7 +3223,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Homepage text'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
},
@@ -3267,7 +3280,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Dialog text"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
@@ -3282,7 +3295,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Secondary dialog text"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
@@ -3406,7 +3419,7 @@ def concatenation_for_salutation(d):
salutation = pgettext("person_name_salutation", salutation)
given_name = None
return " ".join(filter(None, (salutation, title, given_name, family_name)))
return " ".join(str(p) for p in filter(None, (salutation, title, given_name, family_name)))
def get_name_parts_localized(name_parts, key):

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``

View File

@@ -1,7 +1,7 @@
{% load thumb %}
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value.name|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}

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:

View File

@@ -55,12 +55,14 @@ from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
from pytz import common_timezones
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
@@ -79,6 +81,7 @@ from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
class EventWizardFoundationForm(forms.Form):
@@ -538,6 +541,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'region',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
@@ -651,6 +655,9 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only']
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
]
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
self.virtual_keys = []
@@ -931,6 +938,9 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
)
)
self.fields['invoice_generate'].choices = generate_choices
self.fields['invoice_renderer_font'].choices += [
(a, a) for a in get_fonts(event, pdf_support_required=True).keys()
]
def contains_web_channel_validate(val):
@@ -980,7 +990,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will be attached to every email. Available placeholders: {event}"),
validators=[PlaceholderValidator(['{event}'])],
widget_kwargs={'attrs': {
@@ -1003,7 +1013,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1019,7 +1029,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_paid = I18nFormField(
@@ -1030,7 +1040,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_paid = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1046,7 +1056,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_free = I18nFormField(
@@ -1057,7 +1067,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_free = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1073,7 +1083,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_changed = I18nFormField(
@@ -1084,7 +1094,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_resend_link = I18nFormField(
label=_("Subject (sent by admin)"),
@@ -1099,7 +1109,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_resend_all_links = I18nFormField(
label=_("Subject (requested by user)"),
@@ -1109,7 +1119,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_days_order_expire_warning = forms.IntegerField(
label=_("Number of days"),
@@ -1121,7 +1131,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_expire_warning = I18nFormField(
label=_("Text (if order will expire automatically)"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_expire_warning = I18nFormField(
label=_("Subject (if order will expire automatically)"),
@@ -1131,7 +1141,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_pending_warning = I18nFormField(
label=_("Text (if order will not expire automatically)"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_pending_warning = I18nFormField(
label=_("Subject (if order will not expire automatically)"),
@@ -1146,7 +1156,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_incomplete_payment = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This email only applies to payment methods that can receive incomplete payments, "
"such as bank transfer."),
)
@@ -1158,7 +1168,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_payment_failed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_waiting_list = I18nFormField(
label=_("Subject"),
@@ -1168,7 +1178,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_canceled = I18nFormField(
label=_("Subject"),
@@ -1178,12 +1188,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_canceled = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_text_order_custom_mail = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_download_reminder = I18nFormField(
label=_("Subject sent to order contact address"),
@@ -1193,7 +1203,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_download_reminder = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1209,7 +1219,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
@@ -1226,7 +1236,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed_require_approval = I18nFormField(
label=_("Text for received order"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_order_approved = I18nFormField(
label=_("Subject for approved order"),
@@ -1236,7 +1246,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved = I18nFormField(
label=_("Text for approved order"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
@@ -1254,7 +1264,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
@@ -1266,7 +1276,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_free = I18nFormField(
label=_("Text for approved free order"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
@@ -1284,7 +1294,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
@@ -1296,7 +1306,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_denied = I18nFormField(
label=_("Text for denied order"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
base_context = {
'mail_text_order_placed': ['event', 'order', 'payments'],
@@ -1729,7 +1739,7 @@ class ItemMetaPropertyForm(forms.ModelForm):
class ConfirmTextForm(I18nForm):
text = I18nFormField(
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
)

View File

@@ -309,11 +309,8 @@ class OrderFilterForm(FilterForm):
elif s in ('p', 'n', 'e', 'c', 'r'):
qs = qs.filter(status=s)
elif s == 'overpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
)
qs = Order.annotate_overpayments(qs, refunds=False, results=True, sums=False)
qs = qs.filter(is_overpaid=True)
elif s == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
@@ -1234,7 +1231,7 @@ class SubEventFilterForm(FilterForm):
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query) | Q(comment__icontains=query)
)
if fdata.get('date_until'):

View File

@@ -36,10 +36,12 @@ from collections import OrderedDict
from django import forms
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextInput
from pretix import settings
from pretix.base.forms import SecretKeySettingsField, SettingsForm
from pretix.base.forms import (
I18nMarkdownTextarea, SecretKeySettingsField, SettingsForm,
)
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import register_global_settings
@@ -67,12 +69,12 @@ class GlobalSettingsForm(SettingsForm):
help_text=_("Will be included as the link in the additional footer text.")
)),
('banner_message', I18nFormField(
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
required=False,
label=_("Global message banner"),
)),
('banner_message_detail', I18nFormField(
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
required=False,
label=_("Global message banner detail text"),
)),

View File

@@ -56,7 +56,7 @@ from django_scopes.forms import (
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
@@ -82,6 +82,9 @@ class CategoryForm(I18nModelForm):
'description',
'is_addon'
]
widgets = {
'description': I18nMarkdownTextarea,
}
class QuestionForm(I18nModelForm):
@@ -188,6 +191,7 @@ class QuestionForm(I18nModelForm):
attrs={'class': 'scrolling-multiple-choice'}
),
'dependency_values': forms.SelectMultiple,
'help_text': I18nMarkdownTextarea,
}
field_classes = {
'valid_datetime_min': SplitDateTimeField,
@@ -823,6 +827,7 @@ class ItemUpdateForm(I18nModelForm):
'max_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'min_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'checkin_text': forms.TextInput(),
'description': I18nMarkdownTextarea,
}

View File

@@ -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)

View File

@@ -47,11 +47,14 @@ from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext_lazy,
)
from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, MarkdownTextarea,
PlaceholderValidator,
)
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget, format_placeholders_help_text,
@@ -699,7 +702,7 @@ class OrderMailForm(forms.Form):
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=forms.Textarea,
widget=MarkdownTextarea,
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
)
self.fields['attach_invoices'].queryset = order.invoices.all()
@@ -716,7 +719,7 @@ class OrderPositionMailForm(OrderMailForm):
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=forms.Textarea,
widget=MarkdownTextarea,
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
)
self._set_field_placeholders('message', ['event', 'order', 'position'])
@@ -883,7 +886,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
)
self.fields['send_message'] = I18nFormField(
label=_('Message'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
required=True,
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
locales=self.event.settings.get('locales'),
@@ -910,7 +913,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
)
self.fields['send_waitinglist_message'] = I18nFormField(
label=_('Message'),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
required=True,
locales=self.event.settings.get('locales'),
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},

View File

@@ -48,7 +48,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -56,7 +56,9 @@ from pytz import common_timezones
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
get_phone_prefix,
@@ -257,7 +259,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',
@@ -524,7 +526,7 @@ class MailSettingsForm(SettingsForm):
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_("This will be attached to every email."),
validators=[PlaceholderValidator([])],
widget_kwargs={'attrs': {
@@ -543,7 +545,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_registration = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_customer_email_change = I18nFormField(
label=_("Subject"),
@@ -553,7 +555,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_email_change = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
mail_subject_customer_reset = I18nFormField(
label=_("Subject"),
@@ -563,7 +565,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_reset = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
)
base_context = {

View File

@@ -68,6 +68,7 @@ class SubEventForm(I18nModelForm):
'presale_end',
'location',
'frontpage_text',
'comment',
'geo_lat',
'geo_lon',
]
@@ -136,7 +137,7 @@ class SubEventBulkEditForm(I18nModelForm):
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].one_required = False
for k in ('geo_lat', 'geo_lon'):
for k in ('geo_lat', 'geo_lon', 'comment'):
# scalar fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
@@ -166,6 +167,7 @@ class SubEventBulkEditForm(I18nModelForm):
'name',
'location',
'frontpage_text',
'comment',
'geo_lat',
'geo_lon',
'is_public',

View File

@@ -45,7 +45,9 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms import (
I18nModelForm, MarkdownTextarea, PlaceholderValidator,
)
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
@@ -271,7 +273,7 @@ class VoucherBulkForm(VoucherForm):
)
send_message = forms.CharField(
label=_("Message"),
widget=forms.Textarea(attrs={'data-display-dependency': '#id_send'}),
widget=MarkdownTextarea(attrs={'data-display-dependency': '#id_send'}),
required=False,
initial=_('Hello,\n\n'
'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n'

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 '

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'] == '-':

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -28,7 +28,15 @@
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.cardName|default_if_none:payment_info.payment_data.cardType }}</dd>
<dd>
{% if payment_info.payment_data.cardName %}
{{ payment_info.payment_data.cardName }}
{% elif payment_info.payment_data.cardType %}
{{ payment_info.payment_data.cardType }}
{% else %}
{% trans "Unknown" %}
{% endif %}
</dd>
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.expiry }}</dd>
{% endif %}

View File

@@ -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 %}

View File

@@ -7,7 +7,7 @@
{% block title %}{% trans "General settings" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
{% endblock %}
{% block inside %}
<h1>{% trans "General settings" %}</h1>
@@ -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 %}
@@ -361,6 +361,7 @@
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours layout="control" %}
{% bootstrap_field sform.waiting_list_auto_disable layout="control" %}
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}

View File

@@ -19,8 +19,8 @@
section of your website:
{% endblocktrans %}
</p>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}"&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async&gt;&lt;/script&gt;</pre>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}" crossorigin&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async crossorigin&gt;&lt;/script&gt;</pre>
<p>
{% blocktrans trimmed %}
Then, copy the following code to the place of your website where you want the widget to show up:

View File

@@ -267,6 +267,7 @@
{% bootstrap_field form.show_quota_left layout="control" %}
{% for f in plugin_forms %}
{% if not f.is_layouts and not f.title %}
<hr />
{% if f.template and not "template" in f.fields %}
{% include f.template with form=f %}
{% else %}

View File

@@ -124,6 +124,13 @@
</button>
</div>
</form>
{% elif order.status == "e" and order.payment_refund_sum != 0 %}
<div class="alert alert-warning">
{% blocktrans trimmed with amount=order.payment_refund_sum|money:request.event.currency %}
This order is expired even though it received payments of {{ amount }}. You can choose to refund
the money below or reactivate it by extending the payment deadline.
{% endblocktrans %}
</div>
{% endif %}
<div class="row">

View File

@@ -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>

View File

@@ -8,7 +8,7 @@
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
{% endblock %}
{% block content %}
<h1>
@@ -183,6 +183,7 @@
</div>
{% endif %}
<div class="row control-group pdf-info">
<hr/>
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control">
@@ -224,6 +225,7 @@
</div>
</div>
<div class="row control-group pdf-info">
<hr/>
<div class="col-sm-12">
<label>{% trans "Preferred language" %}</label><br>
<select class="form-control" id="pdf-info-locale">

View File

@@ -1,4 +1,5 @@
{% load static %}
@font-face {
font-family: 'AND';
font-style: normal;
@@ -14,7 +15,7 @@
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
{% if "sample" not in style %}
{% if "sample" not in style and "pdf_only" not in style %}
@font-face {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
@@ -27,9 +28,9 @@
{% else %}
font-weight: normal;
{% endif %}
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %}
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %}
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %};
src: {% if "woff2" in formats %}{% if '//' in formats.woff2 %}url('{{ formats.woff2 }}'){% else %}url('{% static formats.woff2 %}'){% endif %} format('woff2'),{% endif %}
{% if "woff" in formats %}{% if '//' in formats.woff %}url('{{ formats.woff }}'){% else %}url('{% static formats.woff %}'){% endif %} format('woff'),{% endif %}
{% if "truetype" in formats %}{% if '//' in formats.truetype %}url('{{ formats.truetype }}'){% else %}url('{% static formats.truetype %}'){% endif %} format('truetype'){% endif %};
}
.preload-font[data-family="{{family}}"][data-style="{{style}}"] {
font-family: '{{ family }}', 'AND';

View File

@@ -70,6 +70,7 @@
</div>
{% bootstrap_field form.frontpage_text layout="bulkedit" %}
{% bootstrap_field form.is_public layout="bulkedit" %}
{% bootstrap_field form.comment layout="bulkedit" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>

View File

@@ -29,6 +29,7 @@
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>

View File

@@ -138,6 +138,9 @@
<small class="text-muted">&middot; {{ k }}: {{ v }}</small>
{% endif %}
{% endfor %}
{% if s.comment %}
<br>{{ s.comment|linebreaksbr }}
{% endif %}
</small>
</td>
<td>

View File

@@ -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 %}

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