Compare commits

...

361 Commits

Author SHA1 Message Date
Raphael Michel
fa8b1c176b Bump to 4.3.0 2021-09-29 11:17:12 +02:00
Richard Schreiber
2598787602 Customer profiles: add minor improvements around disabled fields and margins (#2195) 2021-09-29 10:34:45 +02:00
Raphael Michel
003fa62996 Fix help text 2021-09-28 18:07:34 +02:00
Raphael Michel
798c21955e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4413 of 4413 strings)

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

powered by weblate
2021-09-27 21:52:58 +02:00
Raphael Michel
fe6185af4b Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4413 of 4413 strings)

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

powered by weblate
2021-09-27 21:52:58 +02:00
Raphael Michel
7bacefa442 Update spelling wordlist 2021-09-27 21:47:05 +02:00
Raphael Michel
04e187c297 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-09-27 20:49:18 +02:00
Raphael Michel
9f2ffc3276 Improvements around the waiting list (#2219)
* Waiting list: Support for seated events, pre-fill customer email address

* Allow people to remove themselves

* Update src/pretix/base/settings.py

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

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

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

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

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

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

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

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

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

* Resolve a review note

* Review notes

* Fix linter issues

* Fix import

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-09-27 20:48:02 +02:00
Diego Rodrigo
a9a4cf6fca Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 14.5% (641 of 4403 strings)

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

powered by weblate
2021-09-27 20:26:20 +02:00
ofirtro
a563316e22 Translated on translate.pretix.eu (Hebrew)
Currently translated at 20.4% (35 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/he/

powered by weblate
2021-09-27 20:26:20 +02:00
ofirtro
4b6f55c31d Added translation on translate.pretix.eu (Hebrew) 2021-09-27 20:26:20 +02:00
Klevagruva
21a8fad17a Translated on translate.pretix.eu (Swedish)
Currently translated at 17.0% (749 of 4403 strings)

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

powered by weblate
2021-09-27 20:26:20 +02:00
Raphael Michel
7586df9d3f Docs: Fix note on widget on mobile 2021-09-27 11:34:55 +02:00
Raphael Michel
1d4afa5d27 Sendmail: Fix invalid state if attachment is adde then removed 2021-09-23 17:36:33 +02:00
Richard Schreiber
720d9b924e [Fix] on rtl-languages float productpicture to the right (#2224) 2021-09-23 17:22:50 +02:00
Raphael Michel
9f56669f2a Truelink filter: Allow dots with spaces 2021-09-23 09:50:39 +02:00
Richard Schreiber
fc541016c6 fix typo in logo-image-settings 2021-09-23 08:28:39 +02:00
Raphael Michel
5eefe9ad1e Fix linter issues 2021-09-20 16:51:48 +02:00
Raphael Michel
1d065a7672 Add setting organizer_logo_image_inherit 2021-09-17 13:33:34 +02:00
Raphael Michel
101f5f7781 Rewrite default ticket PDF to make sure caches affected by previous bug are cleaned 2021-09-17 11:55:53 +02:00
Raphael Michel
af7c6d360f Partially revert migration command monkeypatching 2021-09-17 11:07:46 +02:00
Raphael Michel
8751e6e5ba Product list: Show "sold out" before expanding variations 2021-09-17 10:20:43 +02:00
Raphael Michel
93004a8125 Customer detailv iew: Do not show names as "None" 2021-09-17 10:20:43 +02:00
Raphael Michel
adf40e1d56 Refactor our migrate command monkeypatching 2021-09-17 10:20:43 +02:00
Raphael Michel
364cfe0131 Update nginx config for static files 2021-09-17 10:20:43 +02:00
Raphael Michel
1514527ef3 Consistent naming 2021-09-17 10:20:43 +02:00
Tim Neumann
680024234d Dockerfile: Move nginx client_max_body_size to seperate file (#2207) 2021-09-16 12:36:25 +02:00
Richard Schreiber
2a3660f2d1 Fix -- copy answers even when matching customer profiles exist (#2209) 2021-09-16 10:07:43 +02:00
Richard Schreiber
2041d1213a fix address expand button submit-bug 2021-09-16 09:10:20 +02:00
Raphael Michel
42a1fe9bd1 Event settings API: Fix setting confirm_texts 2021-09-15 16:28:57 +02:00
Raphael Michel
002469d523 Fix .po file 2021-09-15 14:47:58 +02:00
pretix translation bot
5be4af1305 Translations update from Weblate (#2205)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (4403 of 4403 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal) (de_Informal))

Currently translated at 100.0% (4403 of 4403 strings)

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

powered by weblate

Co-authored-by: Raphael Michel <michel@rami.io>
2021-09-15 13:54:39 +02:00
Raphael Michel
0b241438e1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-09-15 13:44:42 +02:00
Raphael Michel
61649ab2b8 Self-service cancellation: Allow to disable auto-refunds 2021-09-15 13:43:55 +02:00
Mohamed Tawfiq
848ea999c5 Translated on translate.pretix.eu (Arabic)
Currently translated at 96.4% (165 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ar/

powered by weblate
2021-09-15 13:43:52 +02:00
Mohamed Tawfiq
dfa82870fb Translated on translate.pretix.eu (Arabic)
Currently translated at 88.6% (3891 of 4391 strings)

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

powered by weblate
2021-09-15 13:43:52 +02:00
Mohamed Tawfiq
e05ac7ef34 Translated on translate.pretix.eu (Arabic)
Currently translated at 88.6% (3891 of 4391 strings)

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

powered by weblate
2021-09-15 13:43:52 +02:00
Raphael Michel
ad2334bffc WebHookCall: Increase max URL size 2021-09-15 13:06:25 +02:00
Raphael Michel
17adde99fa Allow to restrict availability of variations by date, sales channel, and voucher (#2202) 2021-09-15 12:04:17 +02:00
Raphael Michel
4789d82c4e Shredder: Fix crash in AttendeeInfoShredder 2021-09-14 15:28:39 +02:00
Raphael Michel
0567e2d22b API: Fix crash on missing require_membership_types property 2021-09-14 15:28:02 +02:00
Raphael Michel
2e0592b0a6 API: Fix crash on invalid input (PRETIXEU-5A9) 2021-09-14 15:02:06 +02:00
Mie Frydensbjerg
7f6d234b4c Translated on translate.pretix.eu (Danish)
Currently translated at 59.0% (101 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/da/

powered by weblate
2021-09-13 12:00:03 +02:00
Mie Frydensbjerg
0436de316b Translated on translate.pretix.eu (Danish)
Currently translated at 36.4% (1600 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Klevagruva
e16d643d2a Translated on translate.pretix.eu (Swedish)
Currently translated at 16.6% (730 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Raphael Michel
bdec22cf3b Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Raphael Michel
b38df27dce Order import: Fix handling of seat IDs 2021-09-09 18:14:43 +02:00
Tim Neumann
b95f556d8f Add config options for max file upload sizes (#2199)
* feat(config): Add config options for max file upload sizes

Closes #2198

* Apply suggestions from code review

Fix docs and comment in settings.py

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>

* Fix import order using isort

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2021-09-09 15:55:06 +02:00
Raphael Michel
851a4c977c Fix inconsistent handling of all_optional 2021-09-08 20:43:56 +02:00
Raphael Michel
7bffd461d1 Allow sales channels to opt out of customer accounts 2021-09-08 20:33:18 +02:00
Richard Schreiber
9a3b4f7863 Subevent: fix overflow for long lines in location 2021-09-08 13:22:57 +02:00
Raphael Michel
673a38ddc8 Cart: Display subevent location and end time in cart (#2191) 2021-09-08 11:24:39 +02:00
Richard Schreiber
a27b8bf213 Subevent: add missing verbose_name for seating plan (#2194) 2021-09-07 09:09:16 +02:00
Raphael Michel
36e6f10b37 Check-in list rule visualization: Fix broken height calculation 2021-09-06 22:35:14 +02:00
Raphael Michel
fde10d7f55 Fix missing license header 2021-09-06 21:14:25 +02:00
Raphael Michel
6b44b2f429 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-06 21:14:11 +02:00
Raphael Michel
5e9018e0fd Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-06 21:14:11 +02:00
Raphael Michel
185f8066ae Fix incorrect part of previous commit 2021-09-06 20:58:40 +02:00
Raphael Michel
6388f7b29c Fix #2192 -- Invoice address name-field always gets overwritten with customer profile 2021-09-06 20:57:45 +02:00
Raphael Michel
4aa2c9d51d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-09-06 20:51:34 +02:00
Raphael Michel
ef9256f0b0 Fix typo in cache key 2021-09-06 20:50:47 +02:00
Raphael Michel
28d78e40f9 Allow to save invoice addresses and attendee profiles to customer account (#2084)
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-09-06 20:50:25 +02:00
Klevagruva
89554a82eb Translated on translate.pretix.eu (Swedish)
Currently translated at 16.2% (708 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
ae99e82ad1 Translated on translate.pretix.eu (Swedish)
Currently translated at 15.1% (663 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
rauxenz
5ea3d01b8d Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (171 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/es/

powered by weblate
2021-09-06 20:18:55 +02:00
rauxenz
aa2bd79b99 Translated on translate.pretix.eu (Spanish)
Currently translated at 68.2% (2977 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
44ee35b885 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.9% (654 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
22b79a8c22 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.7% (643 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
65bbd537e6 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.7% (643 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
34387d7bc0 Translated on translate.pretix.eu (Swedish)
Currently translated at 11.1% (486 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
ityd
ca38204313 Translated on translate.pretix.eu (Spanish)
Currently translated at 68.0% (2969 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
b7083eca2e Translated on translate.pretix.eu (Swedish)
Currently translated at 10.3% (452 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Raphael Michel
6bb8b428dc Stripe: API keys consistently are prefered over connect keys 2021-09-06 20:18:15 +02:00
Raphael Michel
677142d0c9 API: Fix storage of Item.picture 2021-09-06 19:56:57 +02:00
Raphael Michel
d1b66e365a API: Add test case for unsetting settings 2021-09-06 19:33:17 +02:00
Raphael Michel
50154c02ce Voucher: Add error message to form_invalid 2021-09-06 19:32:40 +02:00
Raphael Michel
04375d4fcf Fix voucher form validation (Z#2384192) 2021-09-06 19:32:15 +02:00
Raphael Michel
9c1ff296bb Add missing template 2021-09-06 16:33:41 +02:00
Raphael Michel
0b3acb06b5 Invoice: Fix incorrect reference to original invoice number 2021-09-06 16:33:27 +02:00
Raphael Michel
b2cdccedd6 Docker: Specify distribution of base image, upgrade to Python 3.9 2021-09-05 12:40:21 +02:00
Raphael Michel
7ebefa7b85 Allow to manually bump carts blocking a voucher 2021-08-30 15:57:28 +02:00
Raphael Michel
c7b5baa185 Widget: Only show new tab button on connection error 2021-08-30 15:49:22 +02:00
Raphael Michel
6d08e7a8b0 Docs: libmariadbclient-dev has been replaced by libmariadb-dev 2021-08-30 12:58:15 +02:00
Raphael Michel
0da2b12646 Check-in log exporter: Expose upload time 2021-08-30 12:44:25 +02:00
Raphael Michel
a0693483dc Bump to 4.3.0.dev0 2021-08-27 16:44:36 +02:00
Raphael Michel
29826a9f08 Bump to 4.2.0 2021-08-27 16:44:03 +02:00
Raphael Michel
36a045020f Fix typo check 2021-08-27 16:05:40 +02:00
Raphael Michel
40c2b774aa Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4364 of 4364 strings)

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

powered by weblate
2021-08-27 16:04:05 +02:00
Raphael Michel
8422b2b4aa Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4364 of 4364 strings)

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

powered by weblate
2021-08-27 16:04:05 +02:00
Raphael Michel
ae334c4860 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-08-27 15:05:38 +02:00
Raphael Michel
722f36121d Docs: Fix typo 2021-08-27 15:04:37 +02:00
Richard Schreiber
529092a4ed Order import: warn when char-replacement happens due to unknown encoding (#2184)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-08-27 15:03:35 +02:00
Bruno
e7068020d5 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 14.1% (616 of 4357 strings)

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

powered by weblate
2021-08-27 15:03:19 +02:00
Raphael Michel
08acecf37b Docs: Add algorithm diagrams for checkin 2021-08-27 12:29:21 +02:00
Raphael Michel
b200ca5ad5 Set verbose name of Event.seating_plan 2021-08-24 12:25:42 +02:00
Raphael Michel
e564952148 Web check-in: Show ticket secret on result card 2021-08-24 12:05:45 +02:00
Maarten van den Berg
f4ad2a2293 Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 93.6% (4079 of 4357 strings)

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

powered by weblate
2021-08-24 09:00:42 +02:00
Maarten van den Berg
854bbf26c2 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-24 09:00:42 +02:00
Raphael Michel
ec5a670ea6 PDF: Fix inheritance of answers to add-on products 2021-08-23 18:42:21 +02:00
Raphael Michel
276add9163 Refactor bulk-generation of voucher codes into utility function 2021-08-23 12:50:02 +02:00
Raphael Michel
de977f4818 event_copy_data Signal: Add quota_map argument 2021-08-23 12:03:03 +02:00
Raphael Michel
a4827fc992 Fix sorting of devices by descending name 2021-08-20 14:23:01 +02:00
Fabian
9a002bf172 Docs: no diff in mysql / pstgres in upgrade (#2180) 2021-08-19 12:37:48 +02:00
Raphael Michel
9a7f3e2d8a Stripe: Fix crash in shredder 2021-08-19 12:09:15 +02:00
Raphael Michel
e7546a7575 Manual refunds: Do not attempt to cancel if already canceled 2021-08-19 11:03:45 +02:00
Raphael Michel
434719285b Widget: Make "voucher required" clickable 2021-08-19 10:39:36 +02:00
Raphael Michel
5bc9ba4641 Deleting products: Catch ProtectedError 2021-08-19 10:20:01 +02:00
Raphael Michel
74dd13abd5 Fix unlimited quota issues 2021-08-18 18:40:33 +02:00
Raphael Michel
ead755aa86 Report canceled orders as underpaid if necessary 2021-08-18 17:36:36 +02:00
Raphael Michel
1f46a8b91b Remove print statement 2021-08-18 16:50:03 +02:00
Raphael Michel
eb77c2f6f6 API: Allow bulk-cration of cart positions 2021-08-18 15:28:41 +02:00
Raphael Michel
c5fe615be5 Sendmail: Force preview and show number of orders (#2099) 2021-08-18 15:28:33 +02:00
Raphael Michel
f5504e11ac Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
e88a1a52f9 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
b86d54ea9f Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
b6e2ed14db Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
nd
c513868afa Fix TypeError in Order.ticket_download_available on "empty" orders (#2176)
Co-authored-by: alice <tickets-dev@kulturkosmos.de>
Co-authored-by: nd <git@notandy.de>

Co-authored-by: alice <tickets-dev@kulturkosmos.de>
2021-08-17 23:42:36 +02:00
Raphael Michel
3f7664f743 Subevent bulk-creation: Fix flaky test (#2177) 2021-08-17 08:11:11 +02:00
Raphael Michel
e654b951ed Fix typo in docs 2021-08-16 17:55:33 +02:00
Raphael Michel
b5c7556abe Add FEE_TYPE_INSURANCE 2021-08-16 17:03:57 +02:00
Raphael Michel
53e3619140 Self-service cancellation: Never suggest cancellation fee higher than order total 2021-08-16 14:43:08 +02:00
Raphael Michel
e191988b81 Self-service cancellation: Do not allow to adjust fee on free orders 2021-08-16 14:40:47 +02:00
Raphael Michel
bb7fd9423b Invoice: Auto-resize font of meta data line 2021-08-16 13:07:07 +02:00
Raphael Michel
c10c6ee28d Subevent bulk creation: Resolve AmbiguousTimeError by preferring non-dst time 2021-08-16 13:07:07 +02:00
Raphael Michel
3c64733e93 Fix crash trying to thumbnail invalid image 2021-08-16 13:07:07 +02:00
dependabot[bot]
08cb045f2e Bump path-parse from 1.0.6 to 1.0.7 in /src/pretix/static/npm_dir
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-16 11:35:45 +02:00
Martin Gross
7bf854fe0b Add uninstall-handler for plugins 2021-08-16 11:35:27 +02:00
René Georgi
f2a1e11b85 Translated on translate.pretix.eu (German)
Currently translated at 99.8% (4349 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
amandajurno
9b07912b7f Translated on translate.pretix.eu (Portuguese)
Currently translated at 4.2% (186 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
amandajurno
e1cec9882a Translated on translate.pretix.eu (Portuguese)
Currently translated at 4.2% (185 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Alberto Leoncio
1ff9c1a84b Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 12.7% (556 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Michael
0035825f33 Translated on translate.pretix.eu (Czech)
Currently translated at 99.4% (170 of 171 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Michael
1ec73b1b33 Translated on translate.pretix.eu (Czech)
Currently translated at 4.0% (177 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Svyatoslav
ed83f4558e Translated on translate.pretix.eu (Russian)
Currently translated at 58.4% (100 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ru/

powered by weblate
2021-08-15 19:11:03 +02:00
Svyatoslav
b18ec7605a Translated on translate.pretix.eu (Russian)
Currently translated at 25.2% (1102 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
f96bc0776d Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 100.0% (171 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
629bdcd55d Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 91.5% (3989 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
829fd907a1 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Abdullah
d7fe321f36 Translated on translate.pretix.eu (Arabic)
Currently translated at 89.3% (3891 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
517432319e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Richard Schreiber
edef9f1b23 Control: fix typo to correctly initialize basket’s reservation_time input 2021-08-12 12:39:41 +02:00
Martin Gross
617730ab76 Update Smallscale Docker Docs: bind exposed ports to 127.0.0.1 explicitly 2021-08-10 10:47:14 -04:00
Martin Gross
7c17d041f4 Fix wrong pretix.event.deleted webhook-label 2021-08-10 09:29:58 -04:00
Richard Schreiber
9295abb80e Fix item’s panel-title in checkout questions: move variation to name (#2172) 2021-08-09 15:13:19 +02:00
Richard Schreiber
4c3192f116 Payment availability-check: ignore country if no invoice address given (#2171) 2021-08-09 15:11:49 +02:00
Richard Schreiber
9c6a2eb85a Subevent bulk edit: check if list formset has changed only if not None (#2173) 2021-08-09 15:10:55 +02:00
Martin Gross
bcbc8a542f Update po files
[CI skip]

Signed-off-by: Martin Gross <gross@rami.io>
2021-08-04 13:18:45 -04:00
pretix translation bot
a915442efc Translations update from Weblate (#2167)
* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (170 of 170 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))

Currently translated at 91.7% (3991 of 4351 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))

Currently translated at 78.8% (134 of 170 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate

Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-08-04 13:17:07 -04:00
Richard Schreiber
103631a14b Bundled items: improve label for quantity (#2166)
The label was „Number“, which is rather ambiguous. Changed it to „Quantity“.
2021-08-03 11:24:38 +02:00
Maarten van den Berg
e9d7a24cbf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Maarten van den Berg
cc977e441a Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4347 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Björn Out
add9bae018 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4347 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Hans Fraiponts
77d157ab8e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (170 of 170 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Raphael Michel
e42bc94329 Remove print statement 2021-08-01 17:39:17 +02:00
Raphael Michel
b4bf5f998e Check-in API: Recover from IntegrityError when saving questions 2021-07-31 11:14:36 +02:00
Raphael Michel
dc785e9dac Fix failing tests 2021-07-30 17:34:07 +02:00
Raphael Michel
8f5f95b04e nginx: Do not attempt to parse and rewrite our URLs, please 2021-07-30 16:46:03 +02:00
Raphael Michel
c86839ed41 Translated on translate.pretix.eu (Russian)
Currently translated at 25.3% (1102 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Björn Out
8b6e0f0de7 Translated on translate.pretix.eu (Dutch)
Currently translated at 98.2% (167 of 170 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Björn Out
a65243e4bb Translated on translate.pretix.eu (Dutch)
Currently translated at 99.8% (4344 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Svyatoslav
ac028be84e Translated on translate.pretix.eu (Russian)
Currently translated at 25.3% (1103 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Raphael Michel
efd5b5b1da Thumbnailer: Do not screw with image modes 2021-07-26 17:08:26 +02:00
Raphael Michel
4be618bc93 Thumbnailing: Redefine min-size as exact-size 2021-07-26 15:53:21 +02:00
Raphael Michel
7b6d5a0cc9 Exporters: Allow extended XSLX formatting 2021-07-24 17:35:33 +02:00
Raphael Michel
f367d5e675 Multi-event exports: Pass organizer, fix in staff mode 2021-07-24 16:46:37 +02:00
Raphael Michel
f9b7894c4d Add tab-content stylesheet to frontend 2021-07-24 12:51:21 +02:00
Raphael Michel
354bbb485b Pagination: Add intcomma to number of results 2021-07-24 12:51:21 +02:00
Raphael Michel
8dc5dbd547 PDF: Add placehodler for order code + positionid 2021-07-23 19:29:27 +02:00
Raphael Michel
e04793d2eb pretixSCAN: Add new security profile PretixScanNoSyncSecurityProfile 2021-07-23 18:48:40 +02:00
Raphael Michel
db65c14733 Fix style check failures 2021-07-23 18:14:52 +02:00
Raphael Michel
f10c8b229f Check-in: Ticket should not count as redeemed if only exits have been scanned so far 2021-07-23 11:15:35 +02:00
Raphael Michel
4655d8237f Check-in API: Improve handling of unknown ticket codes 2021-07-23 10:49:33 +02:00
Raphael Michel
78f4f35ca3 Add documentation on certificates plugin 2021-07-21 18:00:33 +02:00
Raphael Michel
3a01a05a08 Check-in history: Fix incorrectly linked product 2021-07-21 10:17:18 +02:00
dedecosta
1738c710cb Translated on translate.pretix.eu (Italian)
Currently translated at 16.5% (720 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
dedecosta
d07783a453 Translated on translate.pretix.eu (Italian)
Currently translated at 100.0% (170 of 170 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/it/

powered by weblate
2021-07-21 09:47:27 +02:00
dedecosta
1ce331f163 Translated on translate.pretix.eu (Italian)
Currently translated at 16.5% (720 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Abdullah
586f95bc6d Translated on translate.pretix.eu (Arabic)
Currently translated at 89.4% (3892 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Frank
5620aec5f2 Translated on translate.pretix.eu (Italian)
Currently translated at 14.4% (629 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Martin Gross
c1dfec20f6 Always register ApplePay domains in livemode (#2154) 2021-07-21 09:41:57 +02:00
Raphael Michel
7fef81bdef Add fix and text for 0f2e90567 2021-07-20 18:14:59 +02:00
Raphael Michel
0f2e905672 Allow to remove seats that have canceled tickets 2021-07-20 17:17:26 +02:00
Raphael Michel
a57a4e7350 Subevent filter: Filter by start time 2021-07-20 09:28:15 +02:00
Raphael Michel
b57a6e982a Order import: Ignore completely empty lines 2021-07-20 08:55:07 +02:00
Raphael Michel
39736ef0d4 Check-in list: Allow to filter by date 2021-07-19 14:27:23 +02:00
Raphael Michel
f7e5f0b567 Order overview: Adjust to new filter style 2021-07-19 13:58:45 +02:00
Raphael Michel
b6078d5272 Allow to filter and sort the list of devices 2021-07-19 13:57:17 +02:00
Raphael Michel
1ed1cd33e8 Consistent styling for filter forms 2021-07-19 13:40:44 +02:00
Raphael Michel
a4a2500725 Subevent list: Allow to filter by time of day or multiple week days 2021-07-19 13:10:21 +02:00
Raphael Michel
3fb44ec9dd Widget: Fix crash if availability is unknown 2021-07-16 13:49:53 +02:00
Raphael Michel
2a96575b4d Event-level domains: Fix customer logout 2021-07-16 13:30:21 +02:00
Raphael Michel
dcf29ec63e Add workaround for issue with "back" button in Safari 2021-07-15 18:38:16 +02:00
Raphael Michel
a743605bd3 Stats: Allow queryset of subevents 2021-07-15 13:21:36 +02:00
Raphael Michel
75dc80eb09 API: Allow to set a custom pagination size 2021-07-13 18:20:12 +02:00
Richard Schreiber
ac16d9d900 Event calendar: Fix duplicate hidden inputs in week select form (#2152) 2021-07-12 16:12:07 +02:00
Raphael Michel
736d26c232 Order import: Fix redirect after import 2021-07-12 16:03:12 +02:00
Richard Schreiber
8985dfc5eb Fix #2150 -- Do not expose internal product names in modify view (#2151) 2021-07-12 15:55:32 +02:00
Martin Gross
bb80ef067a Fix nodejs doku link 2021-07-12 15:09:16 +02:00
Richard Schreiber
bdd9751f0e Widget: remove limit of 50 events in list-view by adding a load-more-button (#2144) 2021-07-10 21:37:27 +02:00
Raphael Michel
965aac6ad5 Add postfix to spellcheck list 2021-07-09 12:01:18 +02:00
Raphael Michel
e3858373d1 Fix missing Event.save() call after setting default plugins 2021-07-09 12:01:18 +02:00
Raphael Michel
fcdfae88d7 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-07-09 11:54:40 +02:00
Raphael Michel
7d5a85e26f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-07-09 11:54:40 +02:00
Raphael Michel
b8b2c2eba3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-07-09 11:37:19 +02:00
Raphael Michel
c6a3280d69 Improve order confirmation message for free orders requiring approval 2021-07-09 11:34:36 +02:00
Raphael Michel
7f9368c415 QuestionStep: Correctly apply order of "initial" values 2021-07-08 09:28:36 +02:00
Raphael Michel
add764e3f0 Update django-i18nfield to 1.9.3 2021-07-06 11:28:45 +02:00
Raphael Michel
a3431cd51e Update django-i18nfield to 1.9.2 2021-07-06 10:31:04 +02:00
Martin Gross
9772d43235 Move PayPal postfix to end of settings list 2021-07-05 15:33:33 +02:00
Richard Schreiber
2e29e369f5 Fix celery progress: assign state/info once to avoid race-conditions (#2142) 2021-07-05 15:09:47 +02:00
Raphael Michel
9f6ce81229 Order import: Support phone number 2021-07-05 10:12:06 +02:00
Martin Gross
d67954de3f Extend WebHook URL to 255 chars (was: 200) 2021-07-05 09:56:50 +02:00
Martin Gross
d04f93d45c Revert the revertion + Fix "PayPal: Add postfix-field and pass information to custom-field" 2021-07-02 13:58:03 +02:00
Raphael Michel
ef70209ba8 Revert "PayPal: Add postfix-field and pass information to custom-field (#2137)"
This reverts commit 32f690e9d0.
2021-07-02 13:50:01 +02:00
Raphael Michel
f127cfc46a Dockerfile: Fix pretix version not known to pip 2021-07-02 10:07:49 +02:00
Raphael Michel
ec444e5bf3 Invoice: Show preview in browser during rebugging 2021-07-02 10:04:30 +02:00
Martin Gross
32f690e9d0 PayPal: Add postfix-field and pass information to custom-field (#2137) 2021-07-02 09:44:38 +02:00
Raphael Michel
9089b630ed Add new settings invoice_regenerate_allowed (#2071) 2021-07-01 14:51:08 +02:00
Richard Schreiber
0c6971ff5f Email: make responsive and show header image in MS Outlook (#2138) 2021-07-01 11:49:30 +02:00
Raphael Michel
59e92245de Merge branch 'release/4.1.x' 2021-06-30 16:39:42 +02:00
Raphael Michel
9894954233 Add static files to check-manifest ignore list 2021-06-30 16:14:27 +02:00
Raphael Michel
6e7505abd5 Add tests/plugins/sendmail to check-manifest ignore 2021-06-30 15:38:31 +02:00
Raphael Michel
9df381ec4c Do not require requirements.txt in GitLab CI 2021-06-30 15:26:53 +02:00
Raphael Michel
be726183cb Bump to 4.2.0.dev0 2021-06-30 15:15:27 +02:00
Raphael Michel
be5e2d8c33 Bump to 4.1.0 2021-06-30 15:14:46 +02:00
Raphael Michel
9da7321a19 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 97.6% (166 of 170 strings)

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

powered by weblate
2021-06-30 15:14:03 +02:00
Raphael Michel
cc7d95b805 Translated on translate.pretix.eu (German)
Currently translated at 97.6% (166 of 170 strings)

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

powered by weblate
2021-06-30 15:14:03 +02:00
Raphael Michel
5376dbffc0 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4342 of 4342 strings)

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

powered by weblate
2021-06-30 15:14:03 +02:00
Raphael Michel
cddefd98d3 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4342 of 4342 strings)

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

powered by weblate
2021-06-30 15:14:03 +02:00
Frank
5ae0e55f7e Translated on translate.pretix.eu (Italian)
Currently translated at 14.5% (629 of 4328 strings)

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

powered by weblate
2021-06-30 15:14:03 +02:00
Raphael Michel
856c36b85a Add "TODO" to German wordlist as well 2021-06-30 15:09:39 +02:00
Raphael Michel
a3bc717a5b Spellcheck: Add "TODO" to word list 2021-06-30 14:55:01 +02:00
Raphael Michel
6fa198a175 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-06-30 14:23:50 +02:00
Raphael Michel
9596f48fed Sendmail: Copy note from rule_update to rule_create 2021-06-30 12:41:34 +02:00
Raphael Michel
11b1c81633 Sendmail: Improve performance of creating rules 2021-06-30 12:41:03 +02:00
Maico Timmerman
212f33afee Widget: Use absolute URL for poweredby URL (#2131)
When the <pretix-widget> was hosted on a separate domain, the
source code URL was pointing to the wrong page. This updates the URL to
always point to the Pretix installation.
2021-06-30 12:40:10 +02:00
Raphael Michel
3fab15d086 Team list: Rewrite search query 2021-06-29 18:06:10 +02:00
Marc-Pascal Clement
2f1dd79162 Docs: Recommend upgrade strategy "eager" (#2135)
Pip's default strategy is to keep the version of all packages which do not explicitly require an upgrade. This caused issues for me for the second time now in the migration step, because some dependencies were not compatible with the new pretix version, but not explicitly listed as a such. Also I think the "eager" strategy better resembles what happens in the docker container build, as it always installs the newest versions.
2021-06-29 14:36:35 +02:00
Raphael Michel
e00ab01235 Teams list: Fix missing pagination 2021-06-29 11:23:39 +02:00
Raphael Michel
60f0e297e3 Order import: Improve subevent handling 2021-06-28 12:13:45 +02:00
Raphael Michel
93c791e16f Thumbnails: Do not convert JPG to PNG images 2021-06-28 09:18:28 +02:00
Raphael Michel
6da8caaa2b Calendar: Do not show next week if a same-day slot is still running 2021-06-27 18:19:03 +02:00
Raphael Michel
38ffd7d6ba OrderListExporter: Fix incorrect order of columns 2021-06-27 10:25:29 +02:00
Raphael Michel
ff4f56392d Fix isort issue 2021-06-25 11:34:44 +02:00
Raphael Michel
618b67ca2f Add meta_noindex setting on organizer level 2021-06-25 11:27:12 +02:00
Raphael Michel
a856a3ef6f Stripe: Fix a rare crash with malformed settings 2021-06-25 11:27:12 +02:00
Raphael Michel
573284c480 PayPal refunds: Actively fetch sale ID if not known (#2134)
Co-authored-by: Martin Gross <gross@rami.io>
2021-06-25 11:12:52 +02:00
Martin Gross
d4712266ff Properly slice the first 10 characters for QR-secret popover hint 2021-06-25 09:32:05 +02:00
Raphael Michel
e4f542b060 Check-in: Return "revoked" code even in online mode 2021-06-22 17:56:50 +02:00
Martin Gross
1b68e8bf0e Fix global invite-url (PRETIXEU-4N7) 2021-06-22 17:52:51 +02:00
Raphael Michel
c8d464ded7 Check-in: Change text represenation of "product" 2021-06-22 17:19:42 +02:00
Raphael Michel
12ab5ace9c Check-in errors: Change text of "result.product" 2021-06-22 17:19:05 +02:00
Raphael Michel
a2126c7b15 Check-in history: Allow to filter for error reasons 2021-06-22 17:18:42 +02:00
Raphael Michel
cba2ad5333 Add Invoice.fee_type, Invoice.fee_internal_type 2021-06-22 16:58:54 +02:00
Raphael Michel
8700c41f5e Event calendar: Forcefully break very long words 2021-06-22 16:48:14 +02:00
Raphael Michel
a88fed283a Customers: Additional filter form fields 2021-06-22 16:30:26 +02:00
Raphael Michel
130ffddf48 Customers: Allow admin to trigger password reset 2021-06-22 16:23:05 +02:00
Raphael Michel
f84b612d7b OrderChange: Fix invalid quota query 2021-06-22 15:51:26 +02:00
Richard Schreiber
e1ac22067a mail: strip image_src before converting to cid 2021-06-22 13:29:25 +02:00
Raphael Michel
60c3b76ee9 Order creation API: Fix possible crash (PRETIXEU-4MS) 2021-06-22 11:00:21 +02:00
Raphael Michel
fa8552e86f Event cancellation: Fix DivisionByZero when cancelling empty events 2021-06-22 10:58:56 +02:00
Raphael Michel
ecf1a40a5e Fix rare crash in async task handling (PRETIXEU-4MB) 2021-06-22 10:57:34 +02:00
Raphael Michel
ecfeae6ad9 OrderPosition.send_mail: Support for attach_ical argument 2021-06-22 10:56:43 +02:00
Raphael Michel
3544c3f5b8 Order change page: Fix pathological performance in event series 2021-06-22 10:50:09 +02:00
Raphael Michel
d8f3a3f5be Waiting list: Warn about functional limits 2021-06-22 10:49:27 +02:00
Raphael Michel
d6849c45fe Fix handling of cancellation requests with free and unpaid orders 2021-06-22 09:41:18 +02:00
Raphael Michel
eaf663794e Translated on translate.pretix.eu (Italian)
Currently translated at 14.5% (629 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Frank
dbbd4fe47f Translated on translate.pretix.eu (Italian)
Currently translated at 14.5% (629 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Raphael Michel
abab7dc874 Translated on translate.pretix.eu (Finnish)
Currently translated at 17.6% (766 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Jaakko Rinta-Filppula
11ddfc511b Translated on translate.pretix.eu (Finnish)
Currently translated at 50.8% (85 of 167 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fi/

powered by weblate
2021-06-22 09:40:34 +02:00
Jaakko Rinta-Filppula
3e50f3dd33 Translated on translate.pretix.eu (Finnish)
Currently translated at 17.6% (766 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Abdullah
bf5becad82 Translated on translate.pretix.eu (Arabic)
Currently translated at 89.9% (3892 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Abdullah
f191ce823a Translated on translate.pretix.eu (Arabic)
Currently translated at 99.4% (166 of 167 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ar/

powered by weblate
2021-06-22 09:40:34 +02:00
Abdullah
b03fed979f Translated on translate.pretix.eu (Arabic)
Currently translated at 89.9% (3892 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Abdullah
91de41b782 Translated on translate.pretix.eu (Arabic)
Currently translated at 89.8% (3888 of 4328 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
digitalesIch
ff1cfe269f Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 99.4% (166 of 167 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
digitalesIch
2641a40142 Translated on translate.pretix.eu (German)
Currently translated at 99.4% (166 of 167 strings)

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

powered by weblate
2021-06-22 09:40:34 +02:00
Maico Timmerman
584d869729 Event creation: call plugin installed() after persisting event (#2133)
With #2089, PluginConfig.installed() is also called for newly created events.
However, the passed event argument is still not committed. The plugins
cannot use the event to insert or select objects, as it does not have an
ID yet.
2021-06-22 09:26:21 +02:00
Maico Timmerman
8b9b86a68d Event setup: do not create new team when staff session active (#2132)
With b260cca412, team provisioning has
been disabled for users that are staff. However, this leads to strange
UX, as a new event created by a staff member, not currently in a staff
session resulted in a 404 directly after creation.

The PR updates this requirement to not need to select a team, only when
a staff session is active.
2021-06-22 09:26:09 +02:00
Raphael Michel
b7f5631ad0 _merge_csp: Avoid duplicate values 2021-06-20 10:12:47 +02:00
Raphael Michel
038413be88 Remove pretix logo from error 500 pages
It triggers to many calls to our support if a self-hosted system is down
where we can do nothing at all...
2021-06-20 09:21:42 +02:00
Raphael Michel
4508745feb Widget: Support for rate-limited servers returning error 429 2021-06-19 18:03:14 +02:00
Raphael Michel
f9fa1733b0 OrganizerIndex cache: ignore cookies starting with __ 2021-06-18 19:28:43 +02:00
Raphael Michel
d50dff4a6e Add missing license header 2021-06-18 19:28:43 +02:00
Tim Neumann
2852722b50 Add redis sentinel support (#1909)
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2021-06-18 19:25:08 +02:00
Raphael Michel
1ef076bb9b Deal with cancelling memberships (#2130) 2021-06-17 18:10:45 +02:00
Raphael Michel
8ad53256c2 Show frontpage_text even in iframes 2021-06-17 11:06:26 +02:00
Raphael Michel
f51155a5df Waiting list emails: Add subevent, subevent_date_from placeholders 2021-06-16 16:54:04 +02:00
Raphael Michel
75f9824095 Run rjsmin over widget.js 2021-06-16 11:51:41 +02:00
Raphael Michel
9678ef3dd4 Fix missing show_names variable 2021-06-15 17:04:30 +02:00
Raphael Michel
4d945cf1e3 Proper fix for 8f05de700 2021-06-15 13:55:40 +02:00
Raphael Michel
8f05de7004 Fix incorrect pre-selection of month select box 2021-06-15 12:25:53 +02:00
Raphael Michel
72388abd57 Performance: Cache complete organizer index page, cache subevent list template fragment (#2125) 2021-06-15 00:02:47 +02:00
Raphael Michel
5801c8602e Revert "Performance: Cache complete organizer index page, cache subevent list template fragment (#2125)"
This reverts commit ba895270fa.
2021-06-14 23:16:19 +02:00
Raphael Michel
eb77f67d28 Fix AttributeError usage when creating external refund matching pending sum 2021-06-14 20:45:15 +02:00
Raphael Michel
ba895270fa Performance: Cache complete organizer index page, cache subevent list template fragment (#2125) 2021-06-14 17:12:11 +02:00
Martin Gross
cd88659351 Show secret in backend ticket barcode popover 2021-06-14 09:40:36 +02:00
Raphael Michel
eabead4768 Email renderers: Improve line height 2021-06-13 21:47:25 +02:00
Raphael Michel
9cb0cf210a Email renderer preview: Do not wrap placeholders in title tags 2021-06-13 21:45:45 +02:00
Raphael Michel
d181241a63 Email renderers: Adjust CSS for compatibility with css-inline 2021-06-13 21:45:28 +02:00
Raphael Michel
b3edb82ffd Performance: Only load ArabicReshaper once 2021-06-13 21:20:35 +02:00
Raphael Michel
eb5ed2bdf9 Performance: Do not load the same TTF font multiple times 2021-06-13 21:20:35 +02:00
Raphael Michel
c132ccd141 Performance: Replace inlinestyler with css_inline; Add drf-ujson (#2123) 2021-06-13 21:20:18 +02:00
Raphael Michel
fb2827e9ab AsyncMixin: Fix occasional crash (PRETIXEU-49P) 2021-06-11 18:09:12 +02:00
Raphael Michel
bb89bf68ef Do not show waiting list if presale is over 2021-06-11 17:58:23 +02:00
Raphael Michel
97d67d58d5 Add Order.custom_followup_at (#2124) 2021-06-11 17:08:13 +02:00
Raphael Michel
3235f90876 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (167 of 167 strings)

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

powered by weblate
2021-06-11 13:12:18 +02:00
Raphael Michel
227b2513b4 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 98.5% (4265 of 4328 strings)

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

powered by weblate
2021-06-11 13:12:18 +02:00
Raphael Michel
5952fdccb8 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4328 of 4328 strings)

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

powered by weblate
2021-06-11 13:12:18 +02:00
dependabot[bot]
4874748aa2 Bump glob-parent from 5.1.1 to 5.1.2 in /src/pretix/static/npm_dir (#2122)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-11 12:56:24 +02:00
Raphael Michel
030ea269b0 Typocheck: Add "OK" to german wordlist 2021-06-11 12:55:50 +02:00
Raphael Michel
d187a497f9 Restructure dependencies (#2121) 2021-06-11 12:51:20 +02:00
Richard Schreiber
efc2efac84 Exporters: Fix crash in error url determination (#2114) 2021-06-09 18:40:21 +02:00
Raphael Michel
3378744a5c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (167 of 167 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Raphael Michel
71ac461929 Translated on translate.pretix.eu (German)
Currently translated at 98.7% (4273 of 4328 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Raphael Michel
039da531c4 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (167 of 167 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Raphael Michel
91e080d962 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (167 of 167 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Raphael Michel
b78cf9f2c5 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 98.4% (4261 of 4328 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Raphael Michel
af68053195 Translated on translate.pretix.eu (German)
Currently translated at 98.4% (4263 of 4328 strings)

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

powered by weblate
2021-06-09 18:39:59 +02:00
Maico Timmerman
2dd1e567cf Event creation: set the organizer locales as default (#2109) 2021-06-09 18:39:44 +02:00
Raphael Michel
33400ed7cc Sendmail: Fix issue during scheduling (PRETIXEU-4FY) 2021-06-09 18:15:41 +02:00
Raphael Michel
fe2e01938a Orders API: Silently fail if an answer to a question is submitted twice 2021-06-09 17:42:39 +02:00
Richard Schreiber
fccd119a1f Waitinglist: fix batch selection, add event.slug to download-filename (#2116)
* fix missing id in inputs for batch delete

* add event.slug to filename
2021-06-09 13:21:02 +02:00
Raphael Michel
b1dee5ae7c AsyncFormView: Pass query string 2021-06-07 16:55:57 +02:00
Raphael Michel
3e178a7293 Calendar: Fix hide/show of selected day contents 2021-06-07 12:52:25 +02:00
Raphael Michel
193407d819 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-06-07 12:48:20 +02:00
lapor-kris
25419dc8e8 Translated on translate.pretix.eu (Slovenian)
Currently translated at 30.0% (1281 of 4261 strings)

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

powered by weblate
2021-06-07 12:47:33 +02:00
Raphael Michel
cec27d7a44 Docs: Fix wrong parameter documentation 2021-06-07 12:42:01 +02:00
Raphael Michel
881f0e04a0 Do not offer to create a new customer account if a membership is required 2021-06-07 12:41:03 +02:00
Raphael Michel
5ee51c8f9a Sendmail: Fix missing gettext() call 2021-06-07 12:40:53 +02:00
Julia Luna
050f3990c3 Sendmail: Allow to disable rules (#2111) 2021-06-07 12:40:01 +02:00
Raphael Michel
b11ae9e5dd Multiple choice fields: Make search optional 2021-06-05 13:01:31 +02:00
Raphael Michel
c7ef79be90 Store all check-in attempts, not only successful ones (#2074) 2021-06-05 13:00:58 +02:00
Raphael Michel
9c3fc69176 Quotas API: Allow to bulk-query availability 2021-06-04 17:16:42 +02:00
Maico Timmerman
18df9d66bb i18n: add config for enabling a set of languages
Adds the `enabled` setting to the `[languages]` table in the pretix.cfg.
If left empty, or  omitted all languages are enabled.
2021-06-04 15:35:09 +02:00
Richard Schreiber
a43625c5e8 fix ignore_for_event_availability for batch edit of subevents (#2107) 2021-06-04 11:02:35 +02:00
Raphael Michel
a4d9d7041c Customer login: Normalize email addresses 2021-06-03 16:46:56 +02:00
Raphael Michel
96eabebc15 Stripe: Improve refund error handling 2021-06-03 16:37:02 +02:00
Raphael Michel
3819df57d8 Fix incorrect session handling in checkout flow 2021-06-03 14:22:50 +02:00
Raphael Michel
0fee7b0613 Sendmail rules: Fix typo 2021-06-03 14:22:42 +02:00
Raphael Michel
1a17f54354 Sendmail rules: Add "missed" state 2021-06-03 13:57:18 +02:00
Raphael Michel
750231eb3c Sendmail rules: Use same labels as manual mail form 2021-06-03 13:55:05 +02:00
Raphael Michel
1bb84b7296 Sendmail rules: Allow 0 days 2021-06-03 13:54:52 +02:00
Raphael Michel
6d9ef397ee Fix bug introduced in PR refactoring 2021-06-03 13:28:45 +02:00
Julia Luna
64d07a2811 Auto-scheduled emails
Co-authored-by: Raphael Michel <michel@rami.io>
2021-06-03 12:47:07 +02:00
Mohamed Tawfiq
e4949b6491 Translated on translate.pretix.eu (Arabic)
Currently translated at 91.2% (3888 of 4261 strings)

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

powered by weblate
2021-06-03 09:30:45 +02:00
Maico Timmerman
71e7df3038 Event API: Add search parameter (#2088)
The events API is paginated with 50 events. When searching for a
specific event within a large Pretix installations UX becomes very bad.
Using this filter users are able to quickly find their event by slug or
their name/location in any language.
2021-06-03 09:25:45 +02:00
Raphael Michel
47df6fe2bc Rich text: Build regex lazily (slight import time improvement) 2021-06-03 09:09:40 +02:00
Raphael Michel
ec08faf205 Docs: Fix a missing colon 2021-06-03 09:08:03 +02:00
Raphael Michel
76e86cbdd1 Fix ignore_for_event_availability in SubEvent bulk editor 2021-06-02 18:24:01 +02:00
Raphael Michel
f1b072b9a4 Fix verification of used seats in is_still_available 2021-06-02 12:35:10 +02:00
Raphael Michel
e792e8fd1e Fix sender address for organizer-level emails 2021-06-02 12:30:50 +02:00
Raphael Michel
bc8b3f504c Event API: Allow to query availability 2021-06-01 19:14:41 +02:00
Raphael Michel
0bcbfda276 Do not propose "-2-2" as a slug when cloning events 2021-06-01 16:07:19 +02:00
Raphael Michel
db029882ec Fix case-sensitive email handling in password recovery 2021-06-01 15:30:36 +02:00
Fabian
a1cc17094d Docs: Note to use UTF8 in PostgreSQL #2096 (#2103)
* Docs: Note to use UTF8 in PostgreSQL #2096

* Docs:  Same like 288f85b but for Docker docs
2021-06-01 13:57:30 +02:00
Raphael Michel
1c763ccce3 Docs: Fix missing typo 2021-06-01 12:28:51 +02:00
Raphael Michel
36fd5e7d01 Require django-countries 7.2 2021-06-01 12:20:13 +02:00
Raphael Michel
66cf9c1ac7 Docs: We do no longer require the [postgres] item 2021-06-01 12:20:13 +02:00
Fabian
0b9b67d603 Docs: Update Python Version in Nginx Config (#2101)
Debian 10 is shipping with Python 3.7 so the Debian 10 based and tested guide should use Python 3.7 in the nginx config.
2021-06-01 08:16:42 +02:00
Raphael Michel
520fb62088 Bump to 4.1.0.dev0 2021-05-31 15:27:31 +02:00
349 changed files with 149693 additions and 88225 deletions

View File

@@ -33,7 +33,8 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell aspell-en
- name: Install Dependencies
run: pip3 install -Ur doc/requirements.txt
run: pip3 install -Ur requirements.txt
working-directory: ./doc
- name: Spellcheck docs
run: make spelling
working-directory: ./doc

View File

@@ -31,7 +31,8 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install -Ur src/requirements.txt
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -56,7 +57,8 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -Ur src/requirements/dev.txt
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -29,7 +29,8 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -Ur src/requirements/dev.txt
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -49,7 +50,8 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -57,7 +57,8 @@ jobs:
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mysql-client
- name: Install Python dependencies
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run checks
run: python manage.py check
working-directory: ./src

View File

@@ -5,8 +5,8 @@ tests:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python manage.py check
- make all compress
- py.test --reruns 3 -n 3 tests
@@ -21,8 +21,8 @@ pypi:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate

View File

@@ -1,9 +1,9 @@
FROM python:3.8
FROM python:3.9-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
libmariadb-dev \
gettext \
git \
libffi-dev \
@@ -15,8 +15,7 @@ RUN apt-get update && \
libxslt1-dev \
locales \
nginx \
python-dev \
python-virtualenv \
python3-virtualenv \
python3-dev \
sudo \
supervisor \
@@ -41,17 +40,14 @@ ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/requirements /pretix/src/requirements
COPY src/requirements.txt /pretix/src
COPY src/setup.py /pretix/src/setup.py
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
pip3 install \
-r requirements.txt \
-r requirements/memcached.txt \
-r requirements/mysql.txt \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
@@ -60,10 +56,11 @@ COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
RUN cd /pretix/src && pip3 install .
RUN cd /pretix/src && python setup.py install
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \

View File

@@ -0,0 +1 @@
client_max_body_size 100M;

View File

@@ -16,7 +16,6 @@ http {
charset utf-8;
tcp_nopush on;
tcp_nodelay on;
client_max_body_size 100M;
log_format private '[$time_local] $host "$request" $status $body_bytes_sent';
@@ -66,9 +65,18 @@ http {
access_log off;
expires 365d;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
gzip on;
}
location / {
proxy_pass http://unix:/tmp/pretix.sock:/;
# Very important:
# proxy_pass http://unix:/tmp/pretix.sock:;
# is not the same as
# proxy_pass http://unix:/tmp/pretix.sock:/;
# In the latter case, nginx will apply its URL parsing, in the former it doesn't.
# There are situations in which pretix' API will deal with "file names" containing %2F%2F, which
# nginx will normalize to %2F, which can break ticket validation.
proxy_pass http://unix:/tmp/pretix.sock:;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}

View File

@@ -297,6 +297,12 @@ to speed up various operations::
[redis]
location=redis://127.0.0.1:6379/1
sessions=false
sentinels=[
["sentinel_host_1", 26379],
["sentinel_host_2", 26379],
["sentinel_host_3", 26379]
]
password=password
``location``
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
@@ -305,13 +311,34 @@ to speed up various operations::
``session``
When this is set to ``True``, redis will be used as the session storage.
``sentinels``
Configures redis sentinels to use.
If you don't want to use redis sentinels, you should omit this option.
If this is set, redis via sentinels will be used instead of plain redis.
In this case the location should be of the form ``redis://my_master/0``.
The ``sentinels`` variable should be a json serialized list of sentinels,
each being a list with the two elements hostname and port.
You cannot provide a password within the location when using sentinels.
Note that the configuration format requires you to either place the entire
value on one line or make sure all values are indented by at least one space.
``password``
If your redis setup doesn't require a password or you already specified it in the location you can omit this option.
If this is set it will be passed to redis as the connection option PASSWORD.
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
Translations
------------
pretix comes with a number of translations. Some of them are marked as "incubating", which means
pretix comes with a number of translations. All languages are enabled by default. If you want to limit
the languages available in your installation, you can enable a set of languages like this::
[languages]
enabled=en,de
Some of the languages them are marked as "incubating", which means
they can usually only be selected in development mode. If you want to use them nevertheless, you
can activate them like this::
@@ -337,11 +364,22 @@ an AMQP server (e.g. RabbitMQ) as a broker and redis or your database as a resul
[celery]
broker=amqp://guest:guest@localhost:5672//
backend=redis://localhost/0
broker_transport_options="{}"
backend_transport_options="{}"
RabbitMQ might be the better choice if you have a complex, multi-server, high-performance setup,
but as you already should have a redis instance ready for session and lock storage, we recommend
redis for convenience. See the `Celery documentation`_ for more details.
The two ``transport_options`` entries can be omitted in most cases.
If they are present they need to be a valid JSON dictionary.
For possible entries in that dictionary see the `Celery documentation`_.
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinal_host_2:26379/0``
and the respective transport_options to ``{"master_name":"mymaster"}``.
If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinal_host_2:26379/0``.
If your redis sentinels themselves have a password set the transport_options to ``{"master_name":"mymaster","sentinel_kwargs":{"password":"my_password"}}``.
Sentry
------
@@ -396,3 +434,19 @@ pretix can make use of some external tools if they are installed. Currently, the
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
Maximum upload file sizes
-------------------------
You can configure the maximum file size for uploading various files::
[pretix_file_upload]
; Max upload size for images in MiB, defaults to 10 MiB
max_size_image = 12
; Max upload size for favicons in MiB, defaults to 1 MiB
max_size_favicon = 2
; Max upload size for email attachments in MiB, defaults to 10 MiB
max_size_email_attachment = 15
; Max upload size for other files in MiB, defaults to 10 MiB
; This includes all file upload type order questions
max_size_other = 100

View File

@@ -39,6 +39,10 @@ Linux and firewalls, we recommend that you start with `ufw`_.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
.. warning:: By default, using `ufw` in conjunction will not have any effect. Please make sure to either bind the exposed
ports of your docker container explicitly to 127.0.0.1 or configure docker to respect any set up firewall
rules.
On this guide
-------------
@@ -58,7 +62,12 @@ Database
--------
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
our database's shell. For PostgreSQL, we would do::
our database's shell. Please make sure that UTF8 is used as encoding for the best compatibility. You can check this with
the following command::
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
For PostgreSQL database creation, we would do::
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
@@ -178,7 +187,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStart=/usr/bin/docker run --name %n -p 8345:80 \
ExecStart=/usr/bin/docker run --name %n -p 127.0.0.1:8345:80 \
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
@@ -228,7 +237,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8345/;
proxy_pass http://localhost:8345;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;

View File

@@ -25,7 +25,7 @@ installation guides):
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
* A `nodejs_` installation
* A `nodejs`_ installation
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
Linux and firewalls, we recommend that you start with `ufw`_.
@@ -51,7 +51,12 @@ Database
--------
Having the database server installed, we still need a database and a database user. We can create these with any kind
of database managing tool or directly on our database's shell. For PostgreSQL, we would do::
of database managing tool or directly on our database's shell. Please make sure that UTF8 is used as encoding for the
best compatibility. You can check this with the following command::
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
For PostgreSQL database creation, we would do::
# sudo -u postgres createuser pretix
# sudo -u postgres createdb -O pretix pretix
@@ -67,7 +72,7 @@ To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
gettext libpq-dev libmariadb-dev libjpeg-dev libopenjp2-7-dev
Config file
-----------
@@ -129,10 +134,13 @@ python installation::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
We now install pretix, its direct dependencies and gunicorn. Replace ``postgres`` with ``mysql`` in the following
command if you're running MySQL::
We now install pretix, its direct dependencies and gunicorn::
(venv)$ pip3 install "pretix[postgres]" gunicorn
(venv)$ pip3 install pretix gunicorn
If you're running MySQL, also install the client library::
(venv)$ pip3 install mysqlclient
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
@@ -229,7 +237,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://localhost:8345/;
proxy_pass http://localhost:8345;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
@@ -251,14 +259,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
}
location /static/ {
alias /var/pretix/venv/lib/python3.5/site-packages/pretix/static.dist/;
alias /var/pretix/venv/lib/python3.7/site-packages/pretix/static.dist/;
access_log off;
expires 365d;
add_header Cache-Control "public";
}
}
.. note:: Remember to replace the ``python3.5`` in the ``/static/`` path in the config
.. note:: Remember to replace the ``python3.7`` in the ``/static/`` path in the config
above with your python version.
We recommend reading about setting `strong encryption settings`_ for your web server.
@@ -277,11 +285,10 @@ Updates
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
``postgres`` with ``mysql`` if necessary)::
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pretix[postgres] gunicorn
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles

View File

@@ -87,7 +87,8 @@ respectively, or ``null`` if there is no such page. You can use those URLs to re
respective page.
The field ``results`` contains a list of objects representing the first results. For most
objects, every page contains 50 results.
objects, every page contains 50 results. You can specify a lower pagination size using the
``page_size`` query parameter, but no more than 50.
Conditional fetching
--------------------

View File

@@ -243,6 +243,99 @@ Cart position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/bulk_create/
Creates multiple new cart position. This operation is deliberately not atomic, so each cart position can succeed
or fail individually, so the response code of the response is not the only thing to look at!
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. warning:: The same limitations as with the regular creation endpoint apply.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/bulk_create/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
[
{
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"given_name": "Peter",
"family_name": "Miller"
},
"attendee_email": null,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
},
{
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"given_name": "Maria",
"family_name": "Miller"
},
"attendee_email": null,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
]
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"results": [
{
"success": true,
"errors": null,
"data": {
"id": 1,
...
},
},
{
"success": "false",
"errors": {
"non_field_errors": ["There is not enough quota available on quota \"Tickets\" to perform the operation."]
},
"data": null
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create positions for
:param event: The ``slug`` field of the event to create positions for
:statuscode 200: See response for success
:statuscode 400: Your input could not be parsed
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
Deletes a cart position, identified by its internal ID.

View File

@@ -362,6 +362,42 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/failed_checkins/
Stores a failed check-in. Only necessary for statistical purposes if you perform scan validation offline.
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
``incomplete``, ``already_redeemed``, or ``error``. Required.
:<json raw_barcode: The raw barcode you scanned. Required.
:<json datetime: Date and time of the scan. Optional.
:<json type: Type of scan, defaults to ``"entry"``.
:<json position: Internal ID of an order position you matched. Optional.
:<json raw_item: Internal ID of an item you matched. Optional.
:<json raw_variation: Internal ID of an item variation you matched. Optional.
:<json raw_subevent: Internal ID of an event series date you matched. Optional.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/failed_checkins/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
{
"raw_barcode": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
"error_reason": "canceled"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param list: The ID of the check-in list to save for
:statuscode 201: no error
:statuscode 400: Invalid request
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position or check-in list does not exist.
Order position endpoints
------------------------
@@ -424,6 +460,9 @@ Order position endpoints
"checkins": [
{
"list": 1,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
@@ -535,6 +574,9 @@ Order position endpoints
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z",
"type": "entry",
"gate": null,
"device": 2,
"auto_checked_in": true
}
],
@@ -562,6 +604,8 @@ Order position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position or check-in list does not exist.
.. _`rest-checkin-redeem`:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/redeem/
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
@@ -576,8 +620,9 @@ Order position endpoints
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in

View File

@@ -84,6 +84,12 @@ Endpoints
The ``clone_from`` parameter has been added to the event creation endpoint.
.. versionchanged:: 4.1
The ``with_availability_for`` parameter has been added.
The ``search`` query parameter has been added to filter events by their slug, name, or location in any language.
.. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -162,6 +168,11 @@ Endpoints
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
set. Please note that this filter will respect default values set on organizer level.
:query sales_channel: If set to a sales channel identifier, only events allowed to be sold on the specified sales channel are returned.
:query with_availability_for: If set to a sales channel identifier, the response will contain a special ``best_availability_state``
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
slow.
:query search: Only return events matching a given search query.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure

View File

@@ -58,6 +58,15 @@ lines list of objects The actual invo
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ fee_type string Fee type, e.g. ``shipping``, ``service``, ``payment``,
``cancellation``, ``giftcard``, or ``other. Can be ``null`` for
all invoice lines
created before this field was introduced as well as for
all lines not created by a fee (e.g. a product).
├ fee_internal_type string Additional fee type, e.g. type of payment provider. Can be ``null``
for all invoice lines
created before this field was introduced as well as for
all lines not created by a fee (e.g. a product).
├ event_date_from datetime Start date of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
@@ -97,6 +106,10 @@ internal_reference string Customer's refe
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
``refers`` now returns an invoice number including the prefix.
.. versionchanged:: 4.1
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
Endpoints
---------
@@ -162,6 +175,8 @@ Endpoints
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"fee_type": null,
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,
@@ -248,6 +263,8 @@ Endpoints
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"fee_type": null,
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,

View File

@@ -26,6 +26,18 @@ description multi-lingual string A public descri
position integer An integer, used for sorting
require_membership boolean If ``true``, booking this variation requires an active membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_until datetime The last date time at which this variation can be bought
(or ``null``).
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
===================================== ========================== =======================================================
Endpoints
@@ -64,6 +76,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": {
"en": "Test2"
},
@@ -129,6 +145,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -160,6 +180,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -181,6 +205,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -233,6 +261,10 @@ Endpoints
"active": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}

View File

@@ -107,6 +107,18 @@ variations list of objects A list with one
├ require_membership boolean If ``true``, booking this variation requires an active membership.
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
Markdown syntax or can be ``null``.
├ sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_until datetime The last date time at which this variation can be bought
(or ``null``).
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
└ position integer An integer, used for sorting
addons list of objects Definition of add-ons that can be chosen for this item.
Only writable during creation,
@@ -230,6 +242,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -241,6 +257,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -337,6 +357,10 @@ Endpoints
"require_membership": false,
"require_membership_types": [],
"description": null,
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"position": 0
},
{
@@ -347,6 +371,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -422,6 +450,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -433,6 +465,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -497,6 +533,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -508,6 +548,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -603,6 +647,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -614,6 +662,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}

View File

@@ -41,6 +41,7 @@ payment_date date **DEPRECATED AN
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
total money (string) Total value of this order
comment string Internal comment on this order
custom_followup_at date Internal date for a custom follow-up action
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if a ticket
of this order is scanned.
@@ -123,6 +124,10 @@ last_modified datetime Last modificati
The ``customer`` attribute has been added.
.. versionchanged:: 4.1
The ``custom_followup_at`` attribute has been added.
.. _order-position-resource:
@@ -160,11 +165,13 @@ secret string Secret code pri
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
checkins list of objects List of **successful** check-ins with this ticket
├ id integer Internal ID of the check-in event
├ list integer Internal ID of the check-in list
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
├ gate integer Internal ID of the gate. Can be ``null``.
├ device integer Internal ID of the device. Can be ``null``.
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
@@ -305,6 +312,7 @@ List of all orders
"fees": [],
"total": "23.00",
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"require_approval": false,
"invoice_address": {
@@ -355,6 +363,8 @@ List of all orders
{
"list": 44,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -474,6 +484,7 @@ Fetching individual orders
"fees": [],
"total": "23.00",
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"require_approval": false,
"invoice_address": {
@@ -524,6 +535,8 @@ Fetching individual orders
{
"list": 44,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -638,6 +651,8 @@ Updating order fields
* ``comment``
* ``custom_followup_at``
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
**Example request**:
@@ -811,6 +826,7 @@ Creating orders
charge will be created), this is just informative in case you *handled the payment already*.
* ``payment_date`` (optional) Date and time of the completion of the payment.
* ``comment`` (optional)
* ``custom_followup_at`` (optional)
* ``checkin_attention`` (optional)
* ``invoice_address`` (optional)
@@ -1421,6 +1437,8 @@ List of all order positions
{
"list": 44,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1527,6 +1545,8 @@ Fetching individual positions
{
"list": 44,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}

View File

@@ -28,12 +28,22 @@ closed boolean Whether the quo
field).
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
have been scanned at an exit.
available boolean Whether this quota is available. Only returned if ``with_availability=true``
is set on the request. Do not rely on this value for critical operations, it may be
slightly out of date.
available_number integer Number of available tickets. Only returned if ``with_availability=true``
is set on the request. Do not rely on this value for critical operations, it may be
slightly out of date. ``null`` means unlimited.
===================================== ========================== =======================================================
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.
.. versionchanged:: 4.1
The ``with_availability`` query parameter has been added.
Endpoints
---------
@@ -80,6 +90,7 @@ Endpoints
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query integer subevent: Only return quotas of the sub-event with the given ID
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -120,6 +131,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the quota to fetch
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.

View File

@@ -82,6 +82,10 @@ Endpoints
The sub-events resource can now be filtered by meta data attributes.
.. versionchanged:: 4.1
The ``with_availability_for`` parameter has been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Returns a list of all sub-events of an event.
@@ -152,6 +156,10 @@ Endpoints
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
have no value set. Please note that this filter will respect default values set on
organizer or event level.
:query with_availability_for: If set to a sales channel identifier, the response will contain a special ``best_availability_state``
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
slow.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -0,0 +1,28 @@
.. spelling: libpretixsync
Check-in algorithms
===================
When a ticket is scanned at the entrance or exit of an event, we follow a series of steps to determine whether
the check-in is allowed or not. To understand some of the terms in the following diagrams, you should also check
out the documentation of the :ref:`ticket redemption API endpoint <rest-checkin-redeem>`.
Server-side
-----------
The following diagram shows the series of checks executed on the server when a ticket is redeemed through the API.
Some simplifications have been made, for example the deduplication mechanism based on the ``nonce`` parameter
to prevent re-uploads of the same scan is not shown.
.. image:: /images/checkin_online.png
Client-side
-----------
The process of verifying tickets offline is a little different. There are two different approaches,
depending on whether we have information about all tickets in the local database. The following diagram shows
the algorithm as currently implemented in recent versions of `libpretixsync`_.
.. image:: /images/checkin_offline.png
.. _libpretixsync: https://github.com/pretix/libpretixsync

View File

@@ -0,0 +1,13 @@
Algorithms
==========
The business logic inside pretix is full of complex algorithms making decisions based on all the hundreds of settings
and input parameters available. Some of them are documented here as graphs, either because fully understanding them is very
when working on features close to them, or because they also need to be re-implemented by client-side components like our
ticket scanning apps and we want to ensure the implementations are as similar as possible to avoid confusion.
.. toctree::
:maxdepth: 2
checkin
layouts

View File

@@ -0,0 +1,15 @@
.. spelling: pretixPOS
Ticket layout
=============
When a ticket is exported to PDF, the system needs to decide which of multiple PDF layouts to use. The
following diagram shows the steps of the decision, showing both the implementation in pretix itself as
well as the implementation in `pretixPOS`_.
The process can be influenced by plugins, which is demonstrated with the example of the shipping plugin.
.. image:: /images/ticket_layouts.png
.. _pretixPOS: https://pretix.eu/about/en/pos

View File

@@ -8,6 +8,7 @@ Developer documentation
setup
contribution/index
implementation/index
translation/index
algorithms/index
api/index
structure
translation/index

View File

@@ -54,7 +54,7 @@ Working with the code
The first thing you need are all the main application's dependencies::
cd src/
pip3 install -r requirements.txt -r requirements/dev.txt
pip3 install -e ".[dev]"
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -0,0 +1,146 @@
@startuml
partition "data-based check" {
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT"
else
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Return error INVALID"
note bottom: TODO\ninconsistent\nwith online\ncheck
else
-down->[yes] "Is the order in status PAID?"
--> if "" then
-right->[no] "Does the check-in list include pending orders?"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is this an entry or exit?"
endif
endif
else
-down->[yes] "Is this an entry or exit?"
endif
endif
endif
endif
"Is this an entry or exit?" --> if "" then
-right->[entry] Evaluate custom logic (rules)
--> if "" then
-right->[error] "Return error RULES"
else
-down->[ok] "Are all required questions answered?"
--> if "" then
-right->[no] "Return error INCOMPLETE"
else
-down->[yes] "Does the check-in list allow multi-entry?"
endif
endif
else
-->[exit] "Return OK "
endif
"Does the check-in list allow multi-entry?" --> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Return error ALREADY_REDEEMED"
endif
endif
endif
endif
}
partition "dataless check" {
"Check based on secret content" --> "Does the secret decode with\nany supported scheme\nand has a valid signature?"
--> if "" then
-down->[yes] "Is the ticket secret on the revocation list?"
--> if "" then
-right->[yes] "Return error REVOKED"
else
-down->[no] "Is the product part of the check-in list? "
--> if "" then
-right->[no] "Return error PRODUCT "
else
-down->[yes] "Is the subevent part of the check-in list? "
--> if "" then
-right->[no] "Return error INVALID "
note bottom: TODO\ninconsistent\nwith online\ncheck
else
--> "Is this an entry or exit? "
endif
endif
endif
else
-right>[no] "Return error INVALID "
endif
"Is this an entry or exit? " --> if "" then
-right->[entry] "Evaluate custom logic (rules) "
--> if "" then
-right->[error] "Return error RULES "
else
-down->[ok] "Are all required questions answered? "
--> if "" then
-right->[no] "Return error INCOMPLETE "
else
-down->[yes] "Does the check-in list allow multi-entry? "
endif
endif
else
-->[exit] " Return OK "
endif
"Does the check-in list allow multi-entry? " --> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Are any locally queued checkins for\nthis ticket of this list known?"
--> if "" then
-right->[no] " Return OK "
else
-down->[yes] "Are all locally queued checkins\nfor this ticket on this list exits? "
--> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last locally\nqueued checkin\nan exit? "
--> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Return error ALREADY_REDEEMED "
endif
endif
endif
endif
}
(*) --> "Check if order position with\nscanned ticket secret exists"
--> if "" then
-down->[yes] "Check based on local database"
else
-->[no] "Check based on secret content"
endif
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -0,0 +1,92 @@
@startuml
(*) --> "Check if order position with\nscanned ticket secret exists"
--> if "" then
-down->[yes] ===CHECK===
else
-->[no] "Check if secret exists\nin revocation list"
--> if "" then
--> "Is this a forced upload?"
--> if "" then
-->[yes] ===CHECK===
else
-right->[no] "Return error REVOKED"
endif
else
-right->[no] "Return error INVALID"
endif
endif
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT"
else
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID\nor is this a forced upload?"
--> if "" then
-right->[no] "Does the check-in list include pending orders?"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
else
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif
endif
"Is this an entry or exit?\nIs the upload forced?" --> if "" then
-right->[entry && not force] Evaluate custom logic (rules)
--> if "" then
-right->[error] "Return error RULES"
else
-down->[ok] "Are all required questions answered?"
--> if "" then
-right->[no && questions_supported] "Return error INCOMPLETE"
else
-down->[yes || not questions_supported] "Does the check-in list allow multi-entry?"
endif
endif
else
-->[exit || force=true] "Return OK "
endif
"Does the check-in list allow multi-entry?" --> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Return error ALREADY_REDEEMED"
endif
endif
endif
endif
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,52 @@
@startuml
(*) --> "Which implementation?"
--> if "" then
-down->[pretixPOS] "Check for TicketLayoutItem with\nsales_channel=pretixpos [libpretixsync]"
--> if "" then
--> (*)
else
-->[not found] "Check for TicketLayoutItem with\nsales_channel=web [libpretixsync]"
--> if "" then
--> (*)
else
-->[not found] "Use event default [libpretixsync]"
--> (*)
endif
endif
else
-right->[pretix] "Check for TicketLayoutItem with\nsales_channel=order.sales_channel"
--> if "" then
-right-> "Run override_layout plugin signal on result"
else
-down->[not found] "Check for TicketLayoutItem with\nsales_channel=web"
--> if "" then
--> "Run override_layout plugin signal on result"
else
-->[not found] "Use event default"
--> "Run override_layout plugin signal on result"
endif
endif
endif
"Run override_layout plugin signal on result" -> (*)
partition pretix_shipping {
"Run override_layout plugin signal on result" --> "Check for ShippingLayoutItem with\nmethod=order.shipping_method"
--> if "" then
--> (*)
else
-down->[not found] "Check for ShippingMethod.layout"
--> if "" then
--> (*)
else
-down->[not found] "Keep original layout"
--> (*)
endif
endif
}
@enduml

View File

@@ -0,0 +1,64 @@
Certificates of attendance
==========================
The certificates plugin provides a HTTP API that allows you to download the certificate for a specific attendee.
Certificate download
--------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/certificate/
Downloads the certificate for one order position, identified by its internal ID. Download is a two-step
process. You will always get a :http:statuscode:`303` response with a ``Location`` header to a different
URL. In the background, our server starts preparing the PDF file.
If you then do a ``GET`` to the URL you were given, you will either receive a :http:statuscode:`409` response
indicating to retry after a few seconds, or a :http:statuscode:`200` response with the PDF file.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 303 See Other
Location: /api/v1/organizers/democon/events/3vjrh/orderpositions/426/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/pdf
...
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the order position to fetch
:statuscode 200: File ready for download
:statuscode 303: Processing started
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.

View File

@@ -15,5 +15,6 @@ If you want to **create** a plugin, please go to the
ticketoutputpdf
badges
campaigns
certificates
digital
webinar

View File

@@ -1,4 +1,4 @@
-r ../src/requirements.txt
-e ../src/
sphinx==2.3.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -21,6 +21,7 @@ cancelled
casted
Ceph
checkbox
checkins
checksum
config
contenttypes

View File

@@ -4,8 +4,7 @@ Embeddable Widget
=================
If you want to show your ticket shop on your event website or blog, you can use our JavaScript widget. This way,
users will not need to leave your site to buy their ticket in most cases. The widget will still open a new tab
for the checkout if the user is on a mobile device.
users will not need to leave your site to buy their ticket in most cases.
To obtain the correct HTML code for embedding your event into your website, we recommend that you go to the "Widget"
tab of your event's settings. You can specify some optional settings there (for example the language of the widget)

2
src/.gitignore vendored
View File

@@ -9,4 +9,4 @@ dist/
*.bak
pretix/static/jsi18n/
node_modules/
.eggs/

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__ = "4.0.0"
__version__ = "4.3.0"

View File

@@ -59,6 +59,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlist-failed_checkins'),
('GET', 'api-v1:checkinlistpos-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
@@ -69,9 +70,9 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
)
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan_online_kiosk'
verbose_name = _('pretixSCAN (kiosk mode, online only)')
verbose_name = _('pretixSCAN (kiosk mode, no order sync, no search)')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
@@ -89,6 +90,37 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlist-failed_checkins'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan_online_noorders'
verbose_name = _('pretixSCAN (online only, no order sync)')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlist-failed_checkins'),
('GET', 'api-v1:checkinlistpos-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
@@ -131,6 +163,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
@@ -158,6 +191,7 @@ DEVICE_SECURITY_PROFILES = {
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -49,7 +49,9 @@ class EventPermission(BasePermission):
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
return False
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
if hasattr(view, '_get_permission_name'):
required_permission = getattr(view, '_get_permission_name')(request)
elif request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
required_permission = getattr(view, 'write_permission')
elif hasattr(view, 'permission'):
required_permission = getattr(view, 'permission')

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-07-05 07:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0005_auto_20191028_1541'),
]
operations = [
migrations.AlterField(
model_name='webhook',
name='target_url',
field=models.URLField(max_length=255),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-09-15 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0006_alter_webhook_target_url'),
]
operations = [
migrations.AlterField(
model_name='webhookcall',
name='target_url',
field=models.URLField(max_length=255),
),
]

View File

@@ -95,7 +95,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
target_url = models.URLField(verbose_name=_("Target URL"))
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
@@ -120,7 +120,7 @@ class WebHookEventListener(models.Model):
class WebHookCall(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
datetime = models.DateTimeField(auto_now_add=True)
target_url = models.URLField()
target_url = models.URLField(max_length=255)
action_type = models.CharField(max_length=255)
is_retry = models.BooleanField(default=False)
execution_time = models.FloatField(null=True)

View File

@@ -0,0 +1,27 @@
#
# 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 rest_framework.pagination import PageNumberPagination
class Pagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = 50

View File

@@ -73,53 +73,61 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
)
with self.context['event'].lock():
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
for quota in new_quotas:
avail = quota.availability(_cache=self.context['quota_cache'])
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
for quota in new_quotas:
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
for quota in new_quotas:
oldsize = self.context['quota_cache'][quota.pk][1]
newsize = oldsize - 1 if oldsize is not None else None
self.context['quota_cache'][quota.pk] = (
Quota.AVAILABILITY_OK if newsize is None or newsize > 0 else Quota.AVAILABILITY_GONE,
newsize
)
validated_data.pop('sales_channel')
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
validated_data.pop('sales_channel')
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
options = answ_data.pop('options')

View File

@@ -42,6 +42,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from django_countries.serializers import CountryFieldMixin
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
@@ -53,7 +54,7 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
from pretix.base.settings import validate_event_settings
from pretix.base.settings import LazyI18nStringList, validate_event_settings
from pretix.base.signals import api_event_settings_fields
logger = logging.getLogger(__name__)
@@ -93,9 +94,12 @@ class MetaPropertyField(Field):
class SeatCategoryMappingField(Field):
def to_representation(self, value):
qs = value.seat_category_mappings.all()
if isinstance(value, Event):
qs = qs.filter(subevent=None)
if hasattr(value, '_seat_category_mappings'):
qs = value._seat_category_mappings
else:
qs = value.seat_category_mappings.all()
if isinstance(value, Event):
qs = qs.filter(subevent=None)
return {
v.layout_category: v.product_id for v in qs
}
@@ -156,6 +160,7 @@ class EventSerializer(I18nAwareModelSerializer):
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
valid_keys = ValidKeysField(source='*', read_only=True)
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
class Meta:
model = Event
@@ -163,12 +168,14 @@ class EventSerializer(I18nAwareModelSerializer):
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
'sales_channels')
'sales_channels', 'best_availability_state')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not hasattr(self.context['request'], 'event'):
self.fields.pop('valid_keys')
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
self.fields.pop('best_availability_state')
def validate(self, data):
data = super().validate(data)
@@ -441,13 +448,19 @@ class SubEventSerializer(I18nAwareModelSerializer):
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
event = SlugRelatedField(slug_field='slug', read_only=True)
meta_data = MetaDataField(source='*')
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
class Meta:
model = SubEvent
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')
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
self.fields.pop('best_availability_state')
def validate(self, data):
data = super().validate(data)
@@ -776,6 +789,10 @@ class EventSettingsSerializer(SettingsSerializer):
data = super().validate(data)
settings_dict = self.instance.freeze()
settings_dict.update(data)
if data.get('confirm_texts') is not None:
data['confirm_texts'] = LazyI18nStringList(data['confirm_texts'])
validate_event_settings(self.event, settings_dict)
return data

View File

@@ -31,9 +31,10 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import os.path
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import QuerySet
@@ -58,7 +59,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types',)
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -73,7 +75,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types',)
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -161,7 +164,7 @@ class ItemSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
), max_size=10 * 1024 * 1024)
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
class Meta:
model = Item
@@ -245,10 +248,13 @@ class ItemSerializer(I18nAwareModelSerializer):
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
item = Item.objects.create(**validated_data)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
for variation_data in variations_data:
require_membership_types = variation_data.pop('require_membership_types')
require_membership_types = variation_data.pop('require_membership_types', [])
v = ItemVariation.objects.create(item=item, **variation_data)
if require_membership_types:
v.require_membership_types.add(*require_membership_types)
@@ -269,7 +275,10 @@ class ItemSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
item = super().update(instance, validated_data)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
# Meta data
if meta_data is not None:
@@ -408,10 +417,19 @@ class QuestionSerializer(I18nAwareModelSerializer):
class QuotaSerializer(I18nAwareModelSerializer):
available = serializers.BooleanField(read_only=True)
available_number = serializers.IntegerField(read_only=True)
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
'release_after_exit', 'available', 'available_number')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'request' not in self.context or self.context['request'].GET.get('with_availability') != 'true':
del self.fields['available']
del self.fields['available_number']
def validate(self, data):
data = super().validate(data)

View File

@@ -26,6 +26,7 @@ from collections import Counter, defaultdict
from decimal import Decimal
import pycountry
from django.conf import settings
from django.core.files import File
from django.db.models import F, Q
from django.utils.timezone import now
@@ -191,7 +192,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
data['options'] = []
@@ -199,7 +200,9 @@ class AnswerSerializer(I18nAwareModelSerializer):
return data
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
if not data.get('question'):
raise ValidationError('Question not specified.')
elif data.get('question').type == Question.TYPE_FILE:
return self._handle_file_upload(data)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
@@ -250,7 +253,30 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'type')
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
class FailedCheckinSerializer(I18nAwareModelSerializer):
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
raw_barcode = serializers.CharField(required=True, allow_null=False)
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
raw_subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
class Meta:
model = Checkin
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
'raw_subevent', 'datetime', 'type', 'position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
event = self.context['event']
self.fields['raw_item'].queryset = event.items.all()
self.fields['raw_variation'].queryset = ItemVariation.objects.filter(item__event=event)
self.fields['position'].queryset = OrderPosition.all.filter(order__event=event)
if event.has_subevents:
self.fields['raw_subevent'].queryset = event.subevents.all()
class OrderDownloadsField(serializers.Field):
@@ -631,7 +657,7 @@ class OrderSerializer(I18nAwareModelSerializer):
model = Order
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer'
)
@@ -662,7 +688,7 @@ class OrderSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'checkin_attention', 'email', 'locale', 'phone']
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -902,6 +928,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
custom_followup_at = serializers.DateField(required=False, allow_null=True)
payment_provider = serializers.CharField(required=False, allow_null=True)
payment_info = CompatibleJSONField(required=False)
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
@@ -920,7 +947,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate', 'customer')
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1269,7 +1296,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
seen_answers = set()
for answ_data in answers_data:
# Workaround for a pretixPOS bug :-(
if answ_data.get('question') in seen_answers:
continue
seen_answers.add(answ_data.get('question'))
options = answ_data.pop('options', [])
if isinstance(answ_data['answer'], File):
@@ -1393,7 +1426,8 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
'fee_internal_type')
class InvoiceSerializer(I18nAwareModelSerializer):

View File

@@ -275,6 +275,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
default_fields = [
'customer_accounts',
'customer_accounts_link_by_email',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',
'organizer_info_text',
@@ -294,6 +295,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'theme_color_background',
'theme_round_borders',
'primary_font',
'organizer_logo_image_inherit',
'organizer_logo_image'
]

View File

@@ -54,8 +54,8 @@ class SettingsSerializer(serializers.Serializer):
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
f._label = str(form_kwargs.get('label', fname))
f._help_text = str(form_kwargs.get('help_text'))
f.parent = self
self.fields[fname] = f

View File

@@ -21,14 +21,18 @@
#
from django.db import transaction
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.settings import api_settings
from pretix.api.serializers.cart import (
CartPositionCreateSerializer, CartPositionSerializer,
)
from pretix.base.models import CartPosition
from pretix.base.services.locking import NoLockManager
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
@@ -50,18 +54,61 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['quota_cache'] = {}
return ctx
def create(self, request, *args, **kwargs):
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
with transaction.atomic(), self.request.event.lock():
self.perform_create(serializer)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@action(detail=False, methods=['POST'])
def bulk_create(self, request, *args, **kwargs):
if not isinstance(request.data, list): # noqa
return Response({"error": "Please supply a list"}, status=status.HTTP_400_BAD_REQUEST)
ctx = self.get_serializer_context()
with transaction.atomic():
serializers = [
CartPositionCreateSerializer(data=d, context=ctx)
for d in request.data
]
lockfn = self.request.event.lock
if not any(s.is_valid(raise_exception=False) for s in serializers):
lockfn = NoLockManager
results = []
with lockfn():
for s in serializers:
if s.is_valid(raise_exception=False):
try:
cp = s.save()
except ValidationError as e:
results.append({
'success': False,
'data': None,
'errors': {api_settings.NON_FIELD_ERRORS_KEY: e.detail},
})
else:
results.append({
'success': True,
'data': CartPositionSerializer(cp, context=ctx).data,
'errors': None,
})
else:
results.append({
'success': False,
'data': None,
'errors': s.errors,
})
return Response({'results': results}, status=status.HTTP_200_OK)
def perform_create(self, serializer):
serializer.save()

View File

@@ -20,7 +20,9 @@
# <https://www.gnu.org/licenses/>.
#
import django_filters
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
)
@@ -31,19 +33,24 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
from pretix.api.serializers.order import (
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
)
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
@@ -79,8 +86,14 @@ class CheckinListViewSet(viewsets.ModelViewSet):
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_class = CheckinListFilter
permission = ('can_view_orders', 'can_checkin_orders',)
write_permission = 'can_change_event_settings'
def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'):
return 'can_checkin_orders', 'can_change_orders'
elif request.method in SAFE_METHODS:
return 'can_view_orders', 'can_checkin_orders',
else:
return 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related(
@@ -125,6 +138,49 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@action(detail=True, methods=['POST'], url_name='failed_checkins')
@transaction.atomic()
def failed_checkins(self, *args, **kwargs):
serializer = FailedCheckinSerializer(
data=self.request.data,
context={'event': self.request.event}
)
serializer.is_valid(raise_exception=True)
kwargs = {}
if not serializer.validated_data.get('position'):
kwargs['position'] = OrderPosition.all.filter(
secret=serializer.validated_data['raw_barcode']
).first()
c = serializer.save(
list=self.get_object(),
successful=False,
forced=True,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
**kwargs,
)
if c.position:
c.position.order.log_action('pretix.event.checkin.denied', data={
'position': c.position.id,
'positionid': c.position.positionid,
'errorcode': c.error_reason,
'reason_explanation': c.error_explanation,
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk
}, user=self.request.user, auth=self.request.auth)
else:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk,
'barcode': c.raw_barcode
}, user=self.request.user, auth=self.request.auth)
return Response(serializer.data, status=201)
@action(detail=True, methods=['GET'])
def status(self, *args, **kwargs):
with language(self.request.event.settings.locale):
@@ -294,7 +350,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'checkins', 'answers', 'answers__options', 'answers__question',
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
@@ -304,7 +360,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
@@ -356,6 +413,18 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
else:
dt = now()
common_checkin_args = dict(
raw_barcode=self.kwargs['pk'],
type=type,
list=self.checkinlist,
datetime=dt,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
nonce=nonce,
forced=force,
)
raw_barcode_for_checkin = None
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
if self.kwargs['pk'].isnumeric():
@@ -364,22 +433,87 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
if len(revoked_matches) == 0 or not force:
if len(revoked_matches) == 0:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
raise Http404()
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
for k, s in self.request.event.ticket_secret_generators.items():
try:
parsed = s.parse_secret(self.kwargs['pk'])
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and isinstance(self.request.auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = self.request.auth.software_brand
ver = parse(self.request.auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
}, status=404)
elif revoked_matches and force:
op = revoked_matches[0].position
raw_barcode_for_checkin = self.kwargs['pk']
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
given_answers = {}
if 'answers' in self.request.data:
@@ -409,6 +543,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
user=self.request.user,
auth=self.request.auth,
type=type,
raw_barcode=raw_barcode_for_checkin,
from_revoked_secret=True,
)
except RequiredQuestionsError as e:
return Response({
@@ -424,11 +560,19 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': dt,
'type': type,
'list': self.checkinlist.pk
}, user=self.request.user, auth=self.request.auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': e.code,
@@ -460,7 +604,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
return cf.file

View File

@@ -34,7 +34,7 @@
import django_filters
from django.db import transaction
from django.db.models import ProtectedError, Q
from django.db.models import Prefetch, ProtectedError, Q
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
@@ -49,20 +49,24 @@ from pretix.api.serializers.event import (
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, TaxRule, TeamAPIToken,
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.i18n import i18ncomp
from pretix.presale.style import regenerate_css
from pretix.presale.views.organizer import filter_qs_by_attr
with scopes_disabled():
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs')
class Meta:
model = Event
@@ -107,6 +111,13 @@ with scopes_disabled():
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(sales_channels__contains=value)
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=i18ncomp(value))
| Q(slug__icontains=value)
| Q(location__icontains=i18ncomp(value))
)
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
@@ -136,10 +147,43 @@ class EventViewSet(viewsets.ModelViewSet):
)
qs = filter_qs_by_attr(qs, self.request)
if 'with_availability_for' in self.request.GET:
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
return qs.prefetch_related(
'meta_values', 'meta_values__property', 'seat_category_mappings'
'organizer',
'meta_values',
'meta_values__property',
'item_meta_properties',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',
queryset=SeatCategoryMapping.objects.filter(subevent=None)
),
)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if 'with_availability_for' in self.request.GET:
quotas_to_compute = []
qcache = {}
for se in page:
se._quota_cache = qcache
quotas_to_compute += se.active_quotas
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute(allow_cache=True)
qcache.update(qa.results)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
def perform_update(self, serializer):
current_live_value = serializer.instance.live
updated_live_value = serializer.validated_data.get('live', None)
@@ -197,7 +241,6 @@ class EventViewSet(viewsets.ModelViewSet):
except Event.DoesNotExist:
raise ValidationError('Event to copy from was not found')
print(copy_from, self.request.GET)
new_event = serializer.save(organizer=self.request.organizer)
if copy_from:
@@ -336,8 +379,18 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
qs = filter_qs_by_attr(qs, self.request)
if 'with_availability_for' in self.request.GET:
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
return qs.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings', 'meta_values'
'event',
'subeventitem_set',
'subeventitemvariation_set',
'meta_values',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',
),
)
def list(self, request, **kwargs):
@@ -345,14 +398,24 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
resp = self.get_paginated_response(serializer.data)
resp['X-Page-Generated'] = date
return resp
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
if 'with_availability_for' in self.request.GET:
quotas_to_compute = []
qcache = {}
for se in page:
se._quota_cache = qcache
quotas_to_compute += se.active_quotas
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute(allow_cache=True)
qcache.update(qa.results)
serializer = self.get_serializer(page, many=True)
resp = self.get_paginated_response(serializer.data)
resp['X-Page-Generated'] = date
return resp
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data

View File

@@ -132,7 +132,7 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event)
for ex in sorted([response(self.request.event) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
@@ -151,7 +151,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex)
return exporters

View File

@@ -477,6 +477,23 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.quotas.all()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if self.request.GET.get('with_availability') == 'true':
if page:
qa = QuotaAvailability()
qa.queue(*page)
qa.compute(allow_cache=False)
for q in page:
q.available = qa.results[q][0] == Quota.AVAILABILITY_OK
q.available_number = qa.results[q][1]
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -496,6 +513,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['request'] = self.request
return ctx
def perform_update(self, serializer):

View File

@@ -55,9 +55,9 @@ from pretix.api.serializers.order import (
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
TaxRule, TeamAPIToken, generate_secret,
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
Quota, SubEvent, TaxRule, TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
from pretix.base.payment import PaymentException
@@ -201,7 +201,8 @@ class OrderViewSet(viewsets.ModelViewSet):
Prefetch(
'positions',
opq.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to', 'seat',
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
)
@@ -212,7 +213,8 @@ class OrderViewSet(viewsets.ModelViewSet):
Prefetch(
'positions',
opq.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
)
)
)
@@ -690,6 +692,16 @@ class OrderViewSet(viewsets.ModelViewSet):
}
)
if 'custom_followup_at' in self.request.data and serializer.instance.custom_followup_at != self.request.data.get('custom_followup_at'):
serializer.instance.log_action(
'pretix.event.order.custom_followup_at',
user=self.request.user,
auth=self.request.auth,
data={
'new_custom_followup_at': self.request.data.get('custom_followup_at')
}
)
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
serializer.instance.log_action(
'pretix.event.order.checkin_attention',
@@ -781,7 +793,7 @@ with scopes_disabled():
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
return queryset.alias(ce=Exists(Checkin.objects.filter(position=OuterRef('pk')))).filter(ce=value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
@@ -835,7 +847,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', qs.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
@@ -845,7 +858,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
Prefetch(
'positions',
qs.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item', 'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
)
)
))
@@ -854,7 +868,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
)
else:
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
)
@@ -1436,8 +1451,14 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
if not inv.event.settings.invoice_regenerate_allowed:
raise PermissionDenied('Invoices may not be changed after they are created.')
elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
elif inv.sent_to_organizer:
raise PermissionDenied('The invoice file has already been exported.')
elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1):
raise PermissionDenied('The invoice file is too old to be regenerated.')
else:
inv = regenerate_invoice(inv)
inv.order.log_action(

View File

@@ -261,7 +261,7 @@ def register_default_webhook_events(sender, **kwargs):
),
ParametrizedEventWebhookEvent(
'pretix.event.deleted',
_('Event details changed'),
_('Event deleted'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.added',

View File

@@ -82,6 +82,13 @@ class SalesChannel:
"""
return False
@property
def customer_accounts_supported(self) -> bool:
"""
If this property is ``True``, checkout will show the customer login step.
"""
return True
def get_all_sales_channels():
global _ALL_CHANNELS

View File

@@ -30,7 +30,7 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.base.templatetags.safelink import safelink as sl
def get_powered_by(safelink=True):
def get_powered_by(request, safelink=True):
gs = GlobalSettingsObject()
d = gs.settings.license_check_input
if d.get('poweredby_name'):
@@ -57,7 +57,7 @@ def get_powered_by(safelink=True):
if d.get('base_license') == 'agpl':
msg += ' (<a href="{}" target="_blank" rel="noopener">{}</a>)'.format(
reverse('source'),
request.build_absolute_uri(reverse('source')),
gettext('source code')
)
@@ -69,7 +69,7 @@ def contextprocessor(request):
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
}
try:
ctx['poweredby'] = get_powered_by(safelink=True)
ctx['poweredby'] = get_powered_by(request, safelink=True)
except Exception:
ctx['poweredby'] = 'powered by <a href="https://pretix.eu/" target="_blank" rel="noopener">pretix</a>'
if settings.DEBUG and 'runserver' not in sys.argv:

View File

@@ -26,6 +26,7 @@ from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
import css_inline
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
@@ -35,7 +36,6 @@ from django.utils.timezone import now
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
from inlinestyler.utils import inline_css
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
@@ -174,7 +174,11 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
htmlctx['ev'] = position.subevent or self.event
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
body_html = tpl.render(htmlctx)
inliner = css_inline.CSSInliner(remove_style_tags=True)
body_html = inliner.inline(body_html)
return body_html
@@ -448,6 +452,26 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalMailTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(

View File

@@ -47,12 +47,15 @@ from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, Cell
from pretix.base.models import Event
def excel_safe(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
@@ -70,8 +73,9 @@ class BaseExporter:
This is the base class for all data exporters
"""
def __init__(self, event, progress_callback=lambda v: None):
def __init__(self, event, organizer, progress_callback=lambda v: None):
self.event = event
self.organizer = organizer
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
if isinstance(event, QuerySet):
@@ -220,9 +224,13 @@ class ListExporter(BaseExporter):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def prepare_xlsx_sheet(self, ws):
pass
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True)
ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws)
try:
ws.title = str(self.verbose_name)
except:

View File

@@ -129,7 +129,7 @@ class OrderListExporter(MultiSheetListExporter):
label=_('End event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or after this date. '
help_text=_('Only include orders including at least one ticket for a date on or before this date. '
'Will also include other dates in case of mixed orders!')
)),
]
@@ -288,6 +288,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
headers.append(_('Follow-up date'))
headers.append(_('Positions'))
headers.append(_('E-mail address verified'))
headers.append(_('Payment providers'))
@@ -393,6 +394,7 @@ class OrderListExporter(MultiSheetListExporter):
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
row.append(order.pcnt)
row.append(_('Yes') if order.email_known_to_work else _('No'))
row.append(', '.join([
@@ -574,6 +576,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Seat row'),
_('Seat number'),
_('Order comment'),
_('Follow-up date'),
]
questions = list(Question.objects.filter(event__in=self.events))
@@ -677,6 +680,7 @@ class OrderListExporter(MultiSheetListExporter):
row += ['', '', '', '', '']
row.append(order.comment)
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
acache = {}
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
@@ -721,7 +725,7 @@ class OrderListExporter(MultiSheetListExporter):
row += [
order.sales_channel,
order.locale,
row.append(_('Yes') if order.email_known_to_work else _('No'))
_('Yes') if order.email_known_to_work else _('No')
]
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
@@ -780,7 +784,7 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment')
_('Status code'), _('Amount'), _('Payment method'), _('Comment'),
]
yield headers

View File

@@ -46,6 +46,7 @@ import vat_moss.errors
import vat_moss.id
from babel import Locale
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -507,7 +508,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
def __init__(self, *args, **kwargs):
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
kwargs.setdefault('max_size', 10 * 1024 * 1024)
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
super().__init__(*args, **kwargs)
@@ -739,7 +740,7 @@ class BaseQuestionsForm(forms.Form):
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
),
max_size=10 * 1024 * 1024,
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
)
elif q.type == Question.TYPE_DATE:
attrs = {}
@@ -976,7 +977,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Name'),
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
initial=self.instance.name_parts,
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
if not event.settings.invoice_name_required:

View File

@@ -184,7 +184,7 @@ class BusinessBooleanRadio(forms.RadioSelect):
self.require_business = require_business
if self.require_business:
choices = (
('business', _('Business customer')),
('business', _('Business or institutional customer')),
)
else:
choices = (

View File

@@ -769,44 +769,55 @@ class Modern1Renderer(ClassicInvoiceRenderer):
]
def _draw_metadata(self, canvas):
# Draws the "invoice number -- date" line. This has gotten a little more complicated since we
# encountered some events with very long invoice numbers. In this case, we automatically reduce
# the font size until it fits.
begin_top = 100 * mm
textobject = canvas.beginText(self.left_margin, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Order code'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
if self.invoice.is_cancellation:
textobject = canvas.beginText(self.left_margin + 50 * mm, self.pagesize[1] - begin_top)
def _draw(label, value, value_size, x, width):
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Cancellation number'))
textobject.textLine(label)
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
textobject.setFont(self.font_regular, value_size)
textobject.textLine(value)
return textobject
textobject = canvas.beginText(self.left_margin + 100 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Original invoice'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
canvas.drawText(textobject)
else:
textobject = canvas.beginText(self.left_margin + 70 * mm, self.pagesize[1] - begin_top)
textobject.textLine(pgettext('invoice', 'Invoice number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
value_size = 10
while value_size >= 5:
objects = [
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
]
p = Paragraph(
date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
if self.invoice.is_cancellation:
objects += [
_draw(pgettext('invoice', 'Cancellation number'), self.invoice.number,
value_size, self.left_margin + 50 * mm, 45 * mm),
_draw(pgettext('invoice', 'Original invoice'), self.invoice.refers.number,
value_size, self.left_margin + 100 * mm, date_x - self.left_margin - 100 * mm - 5 * mm),
]
else:
objects += [
_draw(pgettext('invoice', 'Invoice number'), self.invoice.number,
value_size, self.left_margin + 70 * mm, date_x - self.left_margin - 70 * mm - 5 * mm),
]
if all(objects):
for o in objects:
canvas.drawText(o)
break
value_size -= 1
p = Paragraph(date_format(self.invoice.date, "DATE_FORMAT"), style=self.stylesheet['Normal'])
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
p.drawOn(canvas, date_x, self.pagesize[1] - begin_top - 10 - 6)
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)

View File

@@ -0,0 +1,67 @@
#
# 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/>.
#
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
def monkeypatch_migrations():
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, banlist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct

View File

@@ -103,7 +103,7 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
ex = response(e, report_status)
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')
with open(options['output_file'], 'wb') as f:

View File

@@ -32,53 +32,11 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.core.management.commands.makemigrations import Command as Parent
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, banlist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
from ._migrations import monkeypatch_migrations
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct
monkeypatch_migrations()
class Command(Parent):

View File

@@ -32,12 +32,6 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and filtering it from the output.
"""
import sys
from django.core.management.base import OutputWrapper
@@ -45,9 +39,15 @@ from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and filtering it from the output.
"""
banlist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
"have changes that are not yet reflected",
"re-run 'manage.py migrate'"
)
def write(self, msg, style_func=None, ending=None):

View File

@@ -214,7 +214,7 @@ def _render_csp(h):
def _merge_csp(a, b):
for k, v in a.items():
if k in b:
a[k] += b[k]
a[k] += [i for i in b[k] if i not in a[k]]
for k, v in b.items():
if k not in a:

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-05-24 12:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0190_quota_ignore_for_event_availability'),
]
operations = [
migrations.AddField(
model_name='event',
name='last_modified',
field=models.DateTimeField(auto_now=True, db_index=True),
),
]

View File

@@ -0,0 +1,60 @@
# Generated by Django 3.2.2 on 2021-05-11 16:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0191_event_last_modified'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='checkin',
name='error_explanation',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='checkin',
name='error_reason',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='checkin',
name='raw_barcode',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='checkin',
name='raw_item',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.item'),
),
migrations.AddField(
model_name='checkin',
name='raw_subevent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.subevent'),
),
migrations.AddField(
model_name='checkin',
name='raw_variation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.itemvariation'),
),
migrations.AddField(
model_name='checkin',
name='successful',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='checkin',
name='position',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='all_checkins', to='pretixbase.orderposition'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.3 on 2021-06-11 13:55
import django.db.models.manager
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0192_checkin_more_fields'),
]
operations = [
migrations.AlterModelManagers(
name='checkin',
managers=[
('all', django.db.models.manager.Manager()),
],
),
migrations.AddField(
model_name='order',
name='custom_followup_at',
field=models.DateField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.3 on 2021-06-17 10:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0193_auto_20210611_1355'),
]
operations = [
migrations.AddField(
model_name='membership',
name='canceled',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.3 on 2021-06-22 14:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0194_membership_canceled'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='fee_internal_type',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoiceline',
name='fee_type',
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.2 on 2021-05-23 13:22
import django.db.models.deletion
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0195_auto_20210622_1457'),
]
operations = [
migrations.AddField(
model_name='invoiceaddress',
name='customer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_addresses', to='pretixbase.customer'),
),
migrations.CreateModel(
name='AttendeeProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('attendee_name_cached', models.CharField(max_length=255, null=True)),
('attendee_name_parts', models.JSONField(default=dict)),
('attendee_email', models.EmailField(max_length=254, null=True)),
('company', models.CharField(max_length=255, null=True)),
('street', models.TextField(null=True)),
('zipcode', models.CharField(max_length=30, null=True)),
('city', models.CharField(max_length=255, null=True)),
('country', pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True)),
('state', models.CharField(max_length=255, null=True)),
('answers', models.JSONField(default=list)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendee_profiles', to='pretixbase.customer')),
],
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 3.2.4 on 2021-09-14 08:14
from django.db import migrations, models
import pretix.base.models.fields
import pretix.base.models.items
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0196_auto_20210523_1322'),
]
operations = [
migrations.AddField(
model_name='itemvariation',
name='available_from',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='itemvariation',
name='available_until',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='itemvariation',
name='hide_without_voucher',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='itemvariation',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=pretix.base.models.items._all_sales_channels_identifiers),
),
]

View File

@@ -31,6 +31,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -231,9 +232,14 @@ class CheckinList(LoggedModel):
return rules
class SuccessfulCheckinManager(ScopedManager(organizer='list__event__organizer').__class__):
def get_queryset(self):
return super().get_queryset().filter(successful=True)
class Checkin(models.Model):
"""
A check-in object is created when a person enters or exits the event.
A check-in object is created when a ticket is scanned with our scanning apps.
"""
TYPE_ENTRY = 'entry'
TYPE_EXIT = 'exit'
@@ -241,13 +247,83 @@ class Checkin(models.Model):
(TYPE_ENTRY, _('Entry')),
(TYPE_EXIT, _('Exit')),
)
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins', on_delete=models.CASCADE)
REASON_CANCELED = 'canceled'
REASON_INVALID = 'invalid'
REASON_UNPAID = 'unpaid'
REASON_PRODUCT = 'product'
REASON_RULES = 'rules'
REASON_REVOKED = 'revoked'
REASON_INCOMPLETE = 'incomplete'
REASON_ALREADY_REDEEMED = 'already_redeemed'
REASON_ERROR = 'error'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
(REASON_INVALID, _('Unknown ticket')),
(REASON_UNPAID, _('Ticket not paid')),
(REASON_RULES, _('Forbidden by custom rule')),
(REASON_REVOKED, _('Ticket code revoked/changed')),
(REASON_INCOMPLETE, _('Information required')),
(REASON_ALREADY_REDEEMED, _('Ticket already used')),
(REASON_PRODUCT, _('Ticket type not allowed here')),
(REASON_ERROR, _('Server error')),
)
successful = models.BooleanField(
default=True,
)
error_reason = models.CharField(
max_length=100,
choices=REASONS,
null=True,
blank=True,
)
error_explanation = models.TextField(
null=True,
blank=True,
)
position = models.ForeignKey(
'pretixbase.OrderPosition',
related_name='all_checkins',
on_delete=models.CASCADE,
null=True, blank=True,
)
# For "raw" scans where we do not know which position they belong to (e.g. scan of signed
# barcode that is not in database).
raw_barcode = models.TextField(null=True, blank=True)
raw_item = models.ForeignKey(
'pretixbase.Item',
related_name='checkins',
on_delete=models.SET_NULL,
null=True, blank=True,
)
raw_variation = models.ForeignKey(
'pretixbase.ItemVariation',
related_name='checkins',
on_delete=models.SET_NULL,
null=True, blank=True,
)
raw_subevent = models.ForeignKey(
'pretixbase.SubEvent',
related_name='checkins',
on_delete=models.SET_NULL,
null=True, blank=True,
)
# Datetime of checkin, might be different from created if past scans are uploaded
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
# Datetime of creation on server
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
nonce = models.CharField(max_length=190, null=True, blank=True)
forced = models.BooleanField(default=False)
device = models.ForeignKey(
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
@@ -257,7 +333,8 @@ class Checkin(models.Model):
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
all = ScopedManager(organizer='list__event__organizer')
objects = SuccessfulCheckinManager()
class Meta:
ordering = (('-datetime'),)
@@ -269,7 +346,8 @@ class Checkin(models.Model):
def save(self, **kwargs):
super().save(**kwargs)
self.position.order.touch()
if self.position:
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
@@ -277,3 +355,7 @@ class Checkin(models.Model):
super().delete(**kwargs)
self.position.order.touch()
self.list.touch()
@property
def is_late_upload(self):
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)

View File

@@ -19,19 +19,21 @@
# 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 pycountry
from django.conf import settings
from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.db import models
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned
from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
class Customer(LoggedModel):
@@ -88,6 +90,8 @@ class Customer(LoggedModel):
self.all_logentries().update(data={}, shredded=True)
self.orders.all().update(customer=None)
self.memberships.all().update(attendee_name_parts=None)
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
@scopes_disabled()
def assign_identifier(self):
@@ -174,3 +178,88 @@ class Customer(LoggedModel):
continue
ctx['name_%s' % f] = self.name_parts.get(f, '')
return ctx
@property
def stored_addresses(self):
return self.invoice_addresses(manager='profiles')
class AttendeeProfile(models.Model):
customer = models.ForeignKey(
Customer,
related_name='attendee_profiles',
on_delete=models.CASCADE
)
attendee_name_cached = models.CharField(
max_length=255,
verbose_name=_("Attendee name"),
blank=True, null=True,
)
attendee_name_parts = models.JSONField(
blank=True, default=dict
)
attendee_email = models.EmailField(
verbose_name=_("Attendee email"),
blank=True, null=True,
)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = FastCountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
answers = models.JSONField(default=list)
objects = ScopedManager(organizer='customer__organizer')
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
def describe(self):
from .items import Question
from .orders import QuestionAnswer
parts = [
self.attendee_name,
self.attendee_email,
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
]
for a in self.answers:
value = a.get('value')
try:
value = ", ".join(value.values())
except AttributeError:
value = str(value)
answer = QuestionAnswer(question=Question(type=a.get('question_type')), answer=value)
val = str(answer)
parts.append(f'{a["field_label"]}: {val}')
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import logging
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
@@ -73,9 +74,10 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
logger = logging.getLogger(__name__)
class EventMixin:
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
@@ -154,6 +156,29 @@ class EventMixin:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
def get_time_range_display(self, tz=None, force_show_end=False) -> str:
"""
Returns a formatted string containing the start time and sometimes the end time
of the event with respect to the current locale and to the ``show_date_to``
setting. Dates are not shown. This is usually used in combination with get_date_range_display
"""
tz = tz or self.timezone
show_date_to = self.date_to and (self.settings.show_date_to or force_show_end) and (
# Show date to if start and end are on the same day ("08:00-10:00")
self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() or
# Show date to if start and end are on consecutive days and less than 24h ("23:00-03:00")
(self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() + timedelta(days=1) and
self.date_to.astimezone(tz).time() < self.date_from.astimezone(tz).time())
# Do not show end time if this is a 5-day event because there's no way to make it understandable
)
if show_date_to:
return '{} {}'.format(
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
)
return _date(self.date_from.astimezone(tz), "TIME_FORMAT")
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
@@ -243,6 +268,10 @@ class EventMixin:
).values('items')
sq_active_variation = ItemVariation.objects.filter(
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(hide_without_voucher=False)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
@@ -493,12 +522,18 @@ class Event(EventMixin, LoggedModel):
default=False
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='events')
related_name='events', verbose_name=_('Seating plan'))
last_modified = models.DateTimeField(
auto_now=True, db_index=True
)
sales_channels = MultiStringField(
verbose_name=_('Restrict to specific sales channels'),
help_text=_('Only sell tickets for this event on the following sales channels.'),
default=default_sales_channels,
)
objects = ScopedManager(organizer='organizer')
class Meta:
@@ -529,9 +564,17 @@ class Event(EventMixin, LoggedModel):
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
og_file = self.settings.get('og_image', as_type=str, default='')[7:]
if og_file:
img = get_thumbnail(og_file, '1200').thumb.url
try:
img = get_thumbnail(og_file, '1200').thumb.url
except:
logger.exception(f'Failed to create thumbnail of {og_file}')
img = default_storage.url(og_file)
elif logo_file:
img = get_thumbnail(logo_file, '5000x120').thumb.url
try:
img = get_thumbnail(logo_file, '5000x1200').thumb.url
except:
logger.exception(f'Failed to create thumbnail of {logo_file}')
img = default_storage.url(logo_file)
if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
@@ -736,7 +779,9 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save()
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
quota_map[q.pk] = q
items = list(q.items.all())
vars = list(q.variations.all())
oldid = q.pk
@@ -860,7 +905,7 @@ class Event(EventMixin, LoggedModel):
event_copy_data.send(
sender=self, other=other,
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
question_map=question_map, checkin_list_map=checkin_list_map
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
)
if has_custom_style:
@@ -999,6 +1044,11 @@ class Event(EventMixin, LoggedModel):
| Q(date_to__gte=now() - timedelta(hours=24))
)
) # order_by doesn't make sense with I18nField
if ordering in ("date_ascending", "date_descending"):
# if primary order is by date, then order in database
# this allows to limit/slice results
return subevs.order_by(*orderfields)
for f in reversed(orderfields):
if f.startswith('-'):
subevs = sorted(subevs, key=attrgetter(f[1:]), reverse=True)
@@ -1108,15 +1158,18 @@ class Event(EventMixin, LoggedModel):
self.items.all().delete()
self.subevents.all().delete()
def set_active_plugins(self, modules, allow_restricted=False):
def get_available_plugins(self):
from pretix.base.plugins import get_all_plugins
plugins_active = self.get_plugins()
plugins_available = {
return {
p.module: p for p in get_all_plugins(self)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
def set_active_plugins(self, modules, allow_restricted=False):
plugins_active = self.get_plugins()
plugins_available = self.get_available_plugins()
enable = [m for m in modules if m not in plugins_active and m in plugins_available]
for module in enable:
@@ -1145,6 +1198,10 @@ class Event(EventMixin, LoggedModel):
plugins_active.remove(module)
self.set_active_plugins(plugins_active)
plugins_available = self.get_available_plugins()
if hasattr(plugins_available[module].app, 'uninstalled'):
getattr(plugins_available[module].app, 'uninstalled')(self)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod
@@ -1254,14 +1311,15 @@ class SubEvent(EventMixin, LoggedModel):
verbose_name=_("Frontpage text")
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents')
last_modified = models.DateTimeField(
auto_now=True, db_index=True
)
related_name='subevents', verbose_name=_('Seating plan'))
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
last_modified = models.DateTimeField(
auto_now=True, db_index=True
)
objects = ScopedManager(organizer='event__organizer')
class Meta:

View File

@@ -343,6 +343,8 @@ class InvoiceLine(models.Model):
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
attendee_name = models.TextField(null=True, blank=True)
fee_type = models.CharField(max_length=190, null=True, blank=True)
fee_internal_type = models.CharField(max_length=190, null=True, blank=True)
@property
def net_value(self):

View File

@@ -736,6 +736,11 @@ class Item(LoggedModel):
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
def _all_sales_channels_identifiers():
from pretix.base.channels import get_all_sales_channels
return list(get_all_sales_channels().keys())
class ItemVariation(models.Model):
"""
A variation of a product. For example, if your item is 'T-Shirt'
@@ -761,7 +766,7 @@ class ItemVariation(models.Model):
)
value = I18nCharField(
max_length=255,
verbose_name=_('Description')
verbose_name=_('Variation')
)
active = models.BooleanField(
default=True,
@@ -797,6 +802,29 @@ class ItemVariation(models.Model):
verbose_name=_('Membership types'),
blank=True,
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
help_text=_('This variation will not be sold before the given date.')
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This variation will not be sold after the given date.')
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
'selected here but not on product level, the variation will not be available.'),
blank=True,
)
hide_without_voucher = models.BooleanField(
verbose_name=_('This variation will only be shown if a voucher matching the product is redeemed.'),
default=False,
help_text=_('This variation will be hidden from the event page until the user enters a voucher '
'that unlocks this variation.')
)
objects = ScopedManager(organizer='item__event__organizer')
@@ -928,6 +956,24 @@ class ItemVariation(models.Model):
def is_only_variation(self):
return ItemVariation.objects.filter(item=self.item).count() == 1
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
def is_available(self, now_dt: datetime=None) -> bool:
"""
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
class ItemAddOn(models.Model):
"""
@@ -1051,7 +1097,7 @@ class ItemBundle(models.Model):
)
count = models.PositiveIntegerField(
default=1,
verbose_name=_('Number')
verbose_name=_('Quantity')
)
designated_price = models.DecimalField(
default=Decimal('0.00'), blank=True,
@@ -1620,6 +1666,8 @@ class Quota(LoggedModel):
@staticmethod
def clean_items(event, items, variations):
if not items:
return
for item in items:
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))

View File

@@ -118,6 +118,10 @@ class Membership(models.Model):
verbose_name=_('Test mode'),
default=False
)
canceled = models.BooleanField(
verbose_name=_('Canceled'),
default=False
)
customer = models.ForeignKey(
Customer,
related_name='memberships',

View File

@@ -60,7 +60,7 @@ from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_scopes import ScopedManager, scopes_disabled
@@ -217,6 +217,11 @@ class Order(LockModel, LoggedModel):
help_text=_("The text entered in this field will not be visible to the user and is available for your "
"convenience.")
)
custom_followup_at = models.DateField(
verbose_name=_("Follow-up date"),
help_text=_('We\'ll show you this order to be due for a follow-up on this day.'),
null=True, blank=True
)
checkin_attention = models.BooleanField(
verbose_name=_('Requires special attention'),
default=False,
@@ -300,6 +305,10 @@ class Order(LockModel, LoggedModel):
"""
return self.all_fees(manager='objects')
@property
def custom_followup_due(self):
return self.custom_followup_at and self.custom_followup_at <= now().astimezone(get_current_timezone()).date()
@cached_property
@scopes_disabled()
def count_positions(self):
@@ -410,6 +419,8 @@ 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),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
)
@@ -532,7 +543,7 @@ class Order(LockModel, LoggedModel):
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
return round_decimal(fee, self.event.currency)
return round_decimal(min(fee, self.total), self.event.currency)
@property
@scopes_disabled()
@@ -590,6 +601,8 @@ class Order(LockModel, LoggedModel):
for gc in op.issued_gift_cards.all():
if gc.value != op.price:
return False
if op.granted_memberships.with_usages().filter(usages__gt=0):
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:
@@ -772,6 +785,7 @@ class Order(LockModel, LoggedModel):
def ticket_download_available(self):
return self.event.settings.ticket_download and (
self.event.settings.ticket_download_date is None
or self.ticket_download_date is None
or now() > self.ticket_download_date
) and (
self.status == Order.STATUS_PAID
@@ -899,7 +913,7 @@ class Order(LockModel, LoggedModel):
return str(e)
return True
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
@@ -942,7 +956,7 @@ class Order(LockModel, LoggedModel):
try:
email_content = render_mail(template, context)
subject = subject.format_map(TolerantDict(context))
subject = str(subject).format_map(TolerantDict(context))
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -1689,7 +1703,7 @@ class OrderPayment(models.Model):
})
if self.order.pending_sum + r.amount == Decimal('0.00'):
self.refund.done()
r.done()
return r
@@ -1894,6 +1908,7 @@ class OrderFee(models.Model):
FEE_TYPE_SHIPPING = "shipping"
FEE_TYPE_SERVICE = "service"
FEE_TYPE_CANCELLATION = "cancellation"
FEE_TYPE_INSURANCE = "insurance"
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
@@ -1901,6 +1916,7 @@ class OrderFee(models.Model):
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_OTHER, _("Other fees")),
(FEE_TYPE_GIFTCARD, _("Gift card")),
)
@@ -2054,6 +2070,14 @@ class OrderPosition(AbstractPosition):
def sort_key(self):
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0
@property
def checkins(self):
"""
Related manager for all successful checkins. Use ``all_checkins`` instead if you want
canceled positions as well.
"""
return self.all_checkins(manager='objects')
@property
def generate_ticket(self):
if self.item.generate_tickets is not None:
@@ -2168,7 +2192,7 @@ class OrderPosition(AbstractPosition):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
auth=None, attach_tickets=False, attach_ical=False):
"""
Sends an email to the attendee. Basically, this method does two things:
@@ -2185,6 +2209,7 @@ class OrderPosition(AbstractPosition):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import (
SendMailException, mail, render_mail,
@@ -2204,7 +2229,9 @@ class OrderPosition(AbstractPosition):
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
invoices=invoices, attach_tickets=attach_tickets
invoices=invoices,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
)
except SendMailException:
raise
@@ -2219,6 +2246,7 @@ class OrderPosition(AbstractPosition):
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
}
)
@@ -2306,6 +2334,12 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
customer = models.ForeignKey(
Customer,
related_name='invoice_addresses',
null=True, blank=True,
on_delete=models.CASCADE
)
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
@@ -2332,6 +2366,7 @@ class InvoiceAddress(models.Model):
)
objects = ScopedManager(organizer='order__event__organizer')
profiles = ScopedManager(organizer='customer__organizer')
def save(self, **kwargs):
if self.order:
@@ -2344,6 +2379,20 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
def describe(self):
parts = [
self.company,
self.name,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
self.vat_id,
self.custom_field,
self.internal_reference,
(_('Beneficiary') + ': ' + self.beneficiary) if self.beneficiary else '',
]
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
@property
def is_empty(self):
return (
@@ -2379,6 +2428,30 @@ class InvoiceAddress(models.Model):
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
def for_js(self):
d = {}
if self.name_parts:
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
for i, (k, l, w) in enumerate(scheme['fields']):
d[f'name_parts_{i}'] = self.name_parts.get(k) or ''
d.update({
'company': self.company,
'is_business': self.is_business,
'street': self.street,
'zipcode': self.zipcode,
'city': self.city,
'country': str(self.country) if self.country else None,
'state': str(self.state) if self.state else None,
'vat_id': self.vat_id,
'custom_field': self.custom_field,
'internal_reference': self.internal_reference,
'beneficiary': self.beneficiary,
})
return d
def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)

View File

@@ -38,7 +38,7 @@ from decimal import ROUND_HALF_UP, Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models
from django.db import connection, models
from django.db.models import F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils.crypto import get_random_string
@@ -74,6 +74,55 @@ def generate_code(prefix=None):
return code
def generate_codes(organizer, num=1, prefix=None):
codes = set()
batch_size = 500
if 'postgres' in settings.DATABASES['default']['ENGINE']:
batch_size = 5_000
"""
We're trying to check if any of the requested codes already exists in the database. Generally, this is a
SELECT code FROM voucher WHERE code IN (…)
query. However, it turns out that this query get's rather slow if an organizer has lots of vouchers, even
with a organizer with just over 50_000 vouchers, we've seen that creating 20_000 new voucher codes took
just over 30 seconds. There's another way of doing this query on PostgreSQL, which is joining with a
temporary table
SELECT code FROM voucher INNER JOIN (VALUES …) vals(v) ON (code = v)
This is significantly faster, inserting 20_000 vouchers now takes 2-3s instead of 31s on the same dataset.
It's still slow, and removing the JOIN to the event table doesn't significantly speed it up. We might need
an entirely different approach at some point.
"""
while len(codes) < num:
new_codes = set()
for i in range(min(num - len(codes), batch_size)): # Work around SQLite's SQLITE_MAX_VARIABLE_NUMBER
new_codes.add(_generate_random_code(prefix=prefix))
if 'postgres' in settings.DATABASES['default']['ENGINE']:
with connection.cursor() as cursor:
args = list(new_codes) + [organizer.pk]
tmptable = "VALUES " + (", ".join(['(%s)'] * len(new_codes)))
cursor.execute(
f'SELECT code '
f'FROM "{Voucher._meta.db_table}" '
f'INNER JOIN ({tmptable}) vals(v) ON ("{Voucher._meta.db_table}"."code" = "v")'
f'INNER JOIN "{Event._meta.db_table}" ON ("{Voucher._meta.db_table}"."event_id" = "{Event._meta.db_table}"."id") '
f'WHERE "{Event._meta.db_table}"."organizer_id" = %s',
args
)
for row in cursor.fetchall():
new_codes.remove(row[0])
else:
new_codes -= set([v['code'] for v in Voucher.objects.filter(code__in=new_codes).values('code')])
codes |= new_codes
return list(codes)
class Voucher(LoggedModel):
"""
A Voucher can reserve ticket quota or allow special prices.

View File

@@ -21,8 +21,9 @@
#
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction
from django.db.models import F, Q, Sum
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
@@ -114,9 +115,12 @@ class WaitingListEntry(LoggedModel):
return '%s waits for %s' % (str(self.email), str(self.item))
def clean(self):
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
try:
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
except ObjectDoesNotExist:
raise ValidationError('Invalid input')
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
@@ -147,6 +151,34 @@ class WaitingListEntry(LoggedModel):
)
if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
# to use in combination with seating plans. If your event has 50 seats and a quota of 50 and
# default settings, everything is fine and the waiting list will work as usual. However, as soon
# as those two numbers diverge, either due to misconfiguration or due to intentional features such
# as our COVID-19 minimum distance feature, things get ugly. Theoretically, there could be
# significant quota available but not a single seat! The waiting list would happily send out vouchers
# which do not work at all. Generally, we consider this a "known bug" and not fixable with the current
# design of the waiting list and seating features.
# However, we've put in a simple safeguard that makes sure the waiting list on its own does not screw
# everything up. Specifically, we will not send out vouchers if the number of available seats is less
# than the number of valid vouchers *issued through the waiting list*. Things can still go wrong due to
# manually created vouchers, manually blocked seats or the minimum distance feature, but this reduces
# the possible damage a bit.
num_free_seats_for_product = ev.free_seats().filter(product=self.item).count()
num_valid_vouchers_for_product = self.event.vouchers.filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
if '@' not in self.email:

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import datetime
import re
from collections import defaultdict
from decimal import Decimal, DecimalException
@@ -35,12 +36,14 @@ from django.utils.translation import (
from django_countries import countries
from django_countries.fields import Country
from i18nfield.strings import LazyI18nString
from phonenumber_field.phonenumber import to_python
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.models import (
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
Seat,
Seat, SubEvent,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -155,11 +158,38 @@ class EmailColumn(ImportColumn):
order.email = value
class PhoneColumn(ImportColumn):
identifier = 'phone'
verbose_name = gettext_lazy('Phone number')
def clean(self, value, previous_values):
if value:
if self.event.settings.region in SUPPORTED_REGIONS:
region = self.event.settings.region
elif self.event.settings.locale[:2].upper() in SUPPORTED_REGIONS:
region = self.event.settings.locale[:2].upper()
else:
region = None
phone_number = to_python(value, region)
if not phone_number or not phone_number.is_valid():
raise ValidationError(_('Enter a valid phone number.'))
return phone_number
return value
def assign(self, value, order, position, invoice_address, **kwargs):
order.phone = value
class SubeventColumn(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'))
@@ -172,6 +202,30 @@ class SubeventColumn(ImportColumn):
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 = self.event.timezone.localize(d)
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(
@@ -181,6 +235,8 @@ class SubeventColumn(ImportColumn):
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]
def assign(self, value, order, position, invoice_address, **kwargs):
@@ -622,9 +678,12 @@ class SeatColumn(ImportColumn):
if value:
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:
@@ -725,6 +784,7 @@ def get_all_columns(event):
default.append(SubeventColumn(event))
default += [
EmailColumn(event),
PhoneColumn(event),
ItemColumn(event),
Variation(event),
InvoiceAddressCompany(event),

View File

@@ -703,7 +703,7 @@ class BasePaymentProvider:
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
return True
pass
else:
if str(ia.country) not in restricted_countries:
return False

View File

@@ -50,6 +50,7 @@ from django.conf import settings
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import SimpleLazyObject
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -98,6 +99,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
}),
("order_positionid", {
"label": _("Order code and position number"),
"editor_sample": "A1B2C-1",
"evaluate": lambda orderposition, order, event: f"{orderposition.order.code}-{orderposition.positionid}"
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
@@ -428,7 +434,7 @@ def images_from_questions(sender, *args, **kwargs):
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
a = op.answers.filter(question_id=question_id).first() or a
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
return None
@@ -468,7 +474,7 @@ def variables_from_questions(sender, *args, **kwargs):
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
a = op.answers.filter(question_id=question_id).first() or a
if not a:
return ""
@@ -551,6 +557,12 @@ def get_first_scan(op: OrderPosition):
return ""
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
'delete_harakat': True,
'support_ligatures': False,
}))
class Renderer:
def __init__(self, event, layout, background_file):
@@ -568,6 +580,8 @@ class Renderer:
@classmethod
def _register_fonts(cls):
if hasattr(cls, '_fonts_registered'):
return
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
@@ -582,6 +596,8 @@ class Renderer:
if 'bolditalic' in styles:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
cls._fonts_registered = True
def _draw_poweredby(self, canvas: Canvas, op: OrderPosition, o: dict):
content = o.get('content', 'dark')
if content not in ('dark', 'white'):
@@ -716,11 +732,6 @@ class Renderer:
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
configuration = {
'delete_harakat': True,
'support_ligatures': False,
}
reshaper = ArabicReshaper(configuration=configuration)
try:
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
except:

View File

@@ -22,6 +22,8 @@
import base64
import inspect
import struct
from collections import namedtuple
from typing import Optional
from cryptography.hazmat.backends.openssl.backend import Backend
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
@@ -37,6 +39,8 @@ from pretix.base.models import Item, ItemVariation, SubEvent
from pretix.base.secretgenerators import pretix_sig1_pb2
from pretix.base.signals import register_ticket_secret_generators
ParsedSecret = namedtuple('AnalyzedSecret', 'item variation subevent attendee_name opaque_id')
class BaseTicketSecretGenerator:
"""
@@ -72,6 +76,14 @@ class BaseTicketSecretGenerator:
"""
return False
def parse_secret(self, secret: str) -> Optional[ParsedSecret]:
"""
Given a ``secret``, return an ``ParsedSecret`` with the information decoded from the secret, if possible.
Any value of ``ParsedSecret`` may be ``None``, and if parsing is not possible at all, you can ``None`` (as
the default implementation does).
"""
return None
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
attendee_name: str = None, current_secret: str = None, force_invalidate=False) -> str:
"""
@@ -181,6 +193,15 @@ class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
except:
return None
def parse_secret(self, secret: str) -> Optional[ParsedSecret]:
ticket = self._parse(secret)
if ticket:
item = self.event.items.filter(pk=ticket.item).first() if ticket.item else None
subevent = self.event.subevents.filter(pk=ticket.subevent).first() if ticket.subevent else None
variation = item.variations.filter(pk=ticket.variation).first() if item and ticket.subevent else None
opaque_id = ticket.seed
return self.ParsedSecret(item=item, subevent=subevent, variation=variation, opaque_id=opaque_id, attendee_name=None)
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
current_secret: str = None, force_invalidate=False):
if current_secret and not force_invalidate:

View File

@@ -227,7 +227,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if not self.request.called_directly and counter % max(10, total // 100) == 0:
self.update_state(
state='PROGRESS',
meta={'value': round(counter / total * 100, 2)}
meta={'value': round(counter / total * 100 if total else 0, 2)}
)
except LockTimeoutException:
logger.exception("Could not cancel order")
@@ -285,7 +285,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if not self.request.called_directly and counter % max(10, total // 100) == 0:
self.update_state(
state='PROGRESS',
meta={'value': round(counter / total * 100, 2)}
meta={'value': round(counter / total * 100 if total else 0, 2)}
)
if send_waitinglist:
@@ -296,6 +296,6 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if not self.request.called_directly and counter % max(10, total // 100) == 0:
self.update_state(
state='PROGRESS',
meta={'value': round(counter / total * 100, 2)}
meta={'value': round(counter / total * 100 if total else 0, 2)}
)
return failed

View File

@@ -283,13 +283,16 @@ class CartManager:
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
if (
(op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher)) and
(op.voucher is None or not op.voucher.show_hidden_items)
):
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.active):
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
raise CartError(error_messages['unavailable'])
if self._sales_channel not in op.item.sales_channels:
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():

View File

@@ -39,7 +39,7 @@ import dateutil
import dateutil.parser
import pytz
from django.core.files import File
from django.db import transaction
from django.db import IntegrityError, transaction
from django.db.models import (
BooleanField, Count, ExpressionWrapper, F, IntegerField, OuterRef, Q,
Subquery, Value,
@@ -512,6 +512,21 @@ class RequiredQuestionsError(Exception):
def _save_answers(op, answers, given_answers):
def _create_answer(question, answer):
try:
return op.answers.create(question=question, answer=answer)
except IntegrityError:
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
# here if the user submits their scan a second time while the first one is still running,
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
# again. However, both of these approaches have a significant performance overhead for *all* requests,
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
qa = op.answers.get(question=question)
qa.answer = answer
qa.save(update_fields=['answer'])
qa.options.clear()
written = False
for q, a in given_answers.items():
if not a:
@@ -528,7 +543,7 @@ def _save_answers(op, answers, given_answers):
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=str(a.answer))
qa = _create_answer(question=q, answer=str(a.answer))
qa.options.add(a)
elif isinstance(a, list):
if q in answers:
@@ -538,13 +553,13 @@ def _save_answers(op, answers, given_answers):
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
qa = _create_answer(question=q, answer=", ".join([str(o) for o in a]))
qa.options.add(*a)
elif isinstance(a, File):
if q in answers:
qa = answers[q]
else:
qa = op.answers.create(question=q, answer=str(a))
qa = _create_answer(question=q, answer=str(a))
qa.file.save(os.path.basename(a.name), a, save=False)
qa.answer = 'file://' + qa.file.name
qa.save()
@@ -555,7 +570,7 @@ def _save_answers(op, answers, given_answers):
qa.answer = str(a)
qa.save()
else:
op.answers.create(question=q, answer=str(a))
_create_answer(question=q, answer=str(a))
written = True
if written:
@@ -566,7 +581,8 @@ def _save_answers(op, answers, given_answers):
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY):
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None, from_revoked_secret=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -581,6 +597,11 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
:param nonce: A random nonce to prevent race conditions.
:param datetime: The datetime of the checkin, defaults to now.
"""
# !!!!!!!!!
# Update doc/images/checkin_online.puml if you make substantial changes here!
# !!!!!!!!!
dt = datetime or now()
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
@@ -623,12 +644,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
_('This order is not marked as paid.'),
'unpaid'
)
elif require_answers and not force and questions_supported:
raise RequiredQuestionsError(
_('You need to answer questions to complete this check-in.'),
'incomplete',
require_answers
)
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
rule_data = LazyRuleVars(op, clist, dt)
@@ -643,19 +658,27 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
reason=reason
)
if require_answers and not force and questions_supported:
raise RequiredQuestionsError(
_('You need to answer questions to complete this check-in.'),
'incomplete',
require_answers
)
device = None
if isinstance(auth, Device):
device = auth
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce'))
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
not last_cis or
all(c.type == Checkin.TYPE_EXIT for c in last_cis) or
(clist.allow_entry_after_exit and last_cis[0].type == Checkin.TYPE_EXIT)
)
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
if nonce and ((last_cis and last_cis[0].nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
@@ -667,7 +690,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and not entry_allowed,
forced=force and (not entry_allowed or from_revoked_secret),
raw_barcode=raw_barcode,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
@@ -676,6 +700,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'forced': force or op.order.status != Order.STATUS_PAID,
'datetime': dt,
'type': type,
'answers': {k.pk: str(v) for k, v in given_answers.items()},
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)

View File

@@ -40,7 +40,7 @@ def clean_cart_positions(sender, **kwargs):
cp.delete()
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
cp.delete()
for ia in InvoiceAddress.objects.filter(order__isnull=True, last_modified__lt=now() - timedelta(days=14)):
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
ia.delete()

View File

@@ -56,7 +56,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for receiver, response in responses:
ex = response(event, set_progress)
ex = response(event, event.organizer, set_progress)
if ex.identifier == provider:
d = ex.render(form_data)
if d is None:
@@ -70,12 +70,15 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
def multiexport(self, organizer: Organizer, user: User, device: int, token: int, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
def multiexport(self, organizer: Organizer, user: User, device: int, token: int, fileid: str, provider: str,
form_data: Dict[str, Any], staff_session=False) -> None:
if device:
device = Device.objects.get(pk=device)
if token:
device = TeamAPIToken.objects.get(pk=token)
allowed_events = (device or token or user).get_events_with_permission('can_view_orders')
if user and staff_session:
allowed_events = organizer.events.all()
def set_progress(val):
if not self.request.called_directly:
@@ -100,16 +103,19 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = settings.TIME_ZONE
region = None
with language(locale, region), override(timezone):
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
if form_data.get('events') is not None:
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=form_data.get('events'))
else:
events = allowed_events.filter(pk__in=form_data.get('events'))
events = allowed_events
responses = register_multievent_data_exporters.send(organizer)
for receiver, response in responses:
if not response:
continue
ex = response(events, set_progress)
ex = response(events, organizer, set_progress)
if ex.identifier == provider:
d = ex.render(form_data)
if d is None:

View File

@@ -244,7 +244,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
tax_value=fee.tax_value,
tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else ''
tax_name=fee.tax_rule.name if fee.tax_rule else '',
fee_type=fee.fee_type,
fee_internal_type=fee.internal_type or None,
)
if fee.tax_rule and fee.tax_rule.is_reverse_charge(ia) and fee.value and not fee.tax_value:
@@ -321,7 +323,7 @@ def generate_invoice(order: Order, trigger_pdf=True):
order=order,
event=order.event,
organizer=order.event.organizer,
date=timezone.now().date(),
date=timezone.now().astimezone(order.event.timezone).date(),
)
invoice = build_invoice(invoice)
if trigger_pdf:

View File

@@ -45,7 +45,6 @@ from email.utils import formataddr
from typing import Any, Dict, List, Sequence, Union
from urllib.parse import urljoin, urlparse
import cssutils
import pytz
import requests
from bs4 import BeautifulSoup
@@ -79,7 +78,6 @@ from pretix.presale.ical import get_ical
logger = logging.getLogger('pretix.base.mail')
INVALID_ADDRESS = 'invalid-pretix-mail-address'
cssutils.log.setLevel(logging.CRITICAL)
class TolerantDict(dict):
@@ -176,8 +174,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
subject = str(subject).format_map(TolerantDict(context))
sender = (
sender or
(event.settings.get('mail_from') if event else settings.MAIL_FROM) or
(organizer.settings.get('mail_from') if organizer else settings.MAIL_FROM) or
(event.settings.get('mail_from') if event else None) or
(organizer.settings.get('mail_from') if organizer else None) or
settings.MAIL_FROM
)
if event:
@@ -406,7 +404,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
logger.exception('Could not attach invoice to email')
pass
if attach_size < 4 * 1024 * 1024:
if attach_size < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT:
# Do not attach more than 4MB, it will bounce way to often.
for a in args:
try:
@@ -628,6 +626,7 @@ def encoder_linelength(msg):
def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
image_src = image_src.strip()
try:
if image_src.startswith('data:image/'):
image_type, image_content = image_src.split(',', 1)
@@ -641,13 +640,14 @@ def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
image_src = normalize_image_url(image_src)
path = urlparse(image_src).path
guess_subtype = os.path.splitext(path)[1][1:]
image_type = os.path.splitext(path)[1][1:]
response = requests.get(image_src, verify=verify_ssl)
mime_image = MIMEImage(
response.content, _subtype=guess_subtype)
response.content, _subtype=image_type)
mime_image.add_header('Content-ID', '<%s>' % cid_id)
mime_image.add_header('Content-Disposition', 'inline;\n filename="{}.{}"'.format(cid_id, image_type))
return mime_image
except:

View File

@@ -141,6 +141,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
_('You selected a membership that is connected to a different customer account.')
)
if m.canceled:
raise ValidationError(
_('You selected membership that has been canceled.')
)
if m.testmode != testmode:
raise ValidationError(
_('You can only use a test mode membership for test mode tickets.')

View File

@@ -19,11 +19,11 @@
# 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 css_inline
from django.conf import settings
from django.template.loader import get_template
from django.utils.timezone import override
from django_scopes import scope, scopes_disabled
from inlinestyler.utils import inline_css
from pretix.base.i18n import language
from pretix.base.models import LogEntry, NotificationSetting, User
@@ -131,7 +131,11 @@ def send_notification_mail(notification: Notification, user: User):
}
tpl_html = get_template('pretixbase/email/notification.html')
body_html = inline_css(tpl_html.render(ctx))
body_html = tpl_html.render(ctx)
inliner = css_inline.CSSInliner(remove_style_tags=True)
body_html = inliner.inline(body_html)
tpl_plain = get_template('pretixbase/email/notification.txt')
body_plain = tpl_plain.render(ctx)

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