Compare commits

...

176 Commits

Author SHA1 Message Date
Raphael Michel
fb0ffeeff3 Fix #3281 -- Docker build broken 2023-05-02 10:14:03 +02:00
Raphael Michel
78711f9884 Update .gitlab-ci.yml release script 2023-04-26 15:39:50 +02:00
Raphael Michel
0c46aa42c7 Bump version to 4.19.0 2023-04-26 14:38:05 +02:00
Martin Gross
acefd98ef2 Respect TZ for op.valid_from/valid_until in checkin error messages 2023-04-26 12:39:07 +02:00
Raphael Michel
4cc86c0f24 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5262 of 5262 strings)

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

powered by weblate
2023-04-26 11:55:43 +02:00
Raphael Michel
3bb29f8995 Translations: Update German
Currently translated at 100.0% (5262 of 5262 strings)

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

powered by weblate
2023-04-26 11:55:43 +02:00
Raphael Michel
5724b46224 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5262 of 5262 strings)

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

powered by weblate
2023-04-26 11:55:43 +02:00
Raphael Michel
6bdf521207 Translations: Update German
Currently translated at 100.0% (5262 of 5262 strings)

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

powered by weblate
2023-04-26 11:55:43 +02:00
Raphael Michel
7edb8d098a Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-04-26 11:13:56 +02:00
dependabot[bot]
424a92676a Bump django-localflavor from 3.1 to 4.0 (#3263)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-26 11:12:51 +02:00
dependabot[bot]
6852844410 Bump markdown from 3.3.4 to 3.4.3 (#3268)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-26 11:10:16 +02:00
dependabot[bot]
3d2adf9900 Update importlib-metadata requirement from ==6.5.* to ==6.6.* (#3267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-26 11:07:45 +02:00
Martin Gross
bca3d182ff Respect TZ for op.valid_from/valid_until in Order Data Export 2023-04-26 10:17:12 +02:00
dependabot[bot]
f6890e0e65 Update sphinx requirement from ==6.1.* to ==6.2.* (#3264)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 16:30:31 +02:00
dependabot[bot]
561b94957a Update sphinxcontrib-spelling requirement from ==7.* to ==8.* (#3265)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 16:30:06 +02:00
dependabot[bot]
c6ba50a639 Update pep8-naming requirement from ==0.12.* to ==0.13.* (#3260)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 16:29:58 +02:00
dependabot[bot]
d7849d3ac1 Bump @rollup/plugin-node-resolve from 15.0.1 to 15.0.2 in /src/pretix/static/npm_dir (#3261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 16:29:38 +02:00
Vasco Baleia
efcd3f01bb Translations: Update Portuguese (Portugal)
Currently translated at 93.7% (4930 of 5257 strings)

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

powered by weblate
2023-04-25 15:14:15 +02:00
Raphael Michel
b0385c8325 API: Allow to find orders using their linked reusable medium (#3258) 2023-04-25 14:53:50 +02:00
Raphael Michel
8b0814fe9f Dependabot: Move to pyproject.toml 2023-04-25 14:49:11 +02:00
Raphael Michel
386e658d0b Update developer guide 2023-04-25 10:04:05 +02:00
Raphael Michel
4ef96b7e94 Move build setup to pyproject.toml (#3240) 2023-04-25 10:02:52 +02:00
Raphael Michel
c0c2782db6 Banktransfer: Fix AttributeError 2023-04-25 09:09:12 +02:00
Raphael Michel
141634eb49 Prevent accidental disconnect from Stripe 2023-04-24 18:02:05 +02:00
Raphael Michel
a86dfcd504 Waiting list: Fix language of email context 2023-04-24 14:01:19 +02:00
Raphael Michel
76c6bd57e9 Add tooltip to prices with tax calculation (#3244) 2023-04-24 13:55:17 +02:00
Phin Wolkwitz
73776ce0dd Order approval: Add attendee mail settings (Z#23114617, Z#23118978) (#3234)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-04-24 13:31:03 +02:00
Raphael Michel
4fc6593b60 Update pytest to 7.3 2023-04-24 13:30:39 +02:00
Raphael Michel
08fdf1dcad Update importlib-metadata 2023-04-24 13:30:39 +02:00
Raphael Michel
c2cc49bf34 PDF editor: Upgrade fabric.js (#3196)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-04-24 13:17:11 +02:00
Raphael Michel
0655a7cad1 PDF: Fix valid_from_time placeholder 2023-04-24 11:52:08 +02:00
Raphael Michel
788757b7f0 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5257 of 5257 strings)

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

powered by weblate
2023-04-23 18:51:07 +02:00
Raphael Michel
4bf97a0699 Translations: Update German
Currently translated at 100.0% (5257 of 5257 strings)

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

powered by weblate
2023-04-23 18:51:07 +02:00
Raphael Michel
74a55bfe0e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5257 of 5257 strings)

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

powered by weblate
2023-04-23 18:51:07 +02:00
Raphael Michel
fe3422edc8 Translations: Update German
Currently translated at 100.0% (5257 of 5257 strings)

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

powered by weblate
2023-04-23 18:51:07 +02:00
Raphael Michel
a166ebeb07 Translations: Update wordlist 2023-04-23 18:35:13 +02:00
Raphael Michel
2dfd507134 Order change: Allow to add bundled products later on 2023-04-23 18:25:44 +02:00
Raphael Michel
89da42e98c Order detail view: Highlight the products that require approval 2023-04-23 18:14:50 +02:00
Raphael Michel
d42a937d39 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-04-23 18:02:03 +02:00
Raphael Michel
c2d47ca7d3 Clarify text of ticket_download_addons 2023-04-23 18:01:07 +02:00
Yvan Cadoux
0570b51324 Translations: Update French
Currently translated at 49.4% (2598 of 5250 strings)

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

powered by weblate
2023-04-23 18:01:04 +02:00
Raphael Michel
b19d339c37 Revert "Provide hidpi versions of logos and product pictures (#3235)"
This reverts commit 044d6720d2.
2023-04-21 10:27:59 +02:00
Raphael Michel
beea439df8 Fix SVG QR code generation 2023-04-20 13:42:58 +02:00
Raphael Michel
30a2d853fd Order search: Extend zfill() for invoice numbers beyond 5 digits 2023-04-18 21:45:18 +02:00
Michael Stapelberg
044d6720d2 Provide hidpi versions of logos and product pictures (#3235)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-04-18 13:58:23 +02:00
Raphael Michel
2427421945 Migrate from pkg_resources to importlib (#3232) 2023-04-18 12:46:13 +02:00
Raphael Michel
ff86fcf000 Add session pinning by country (#3233) 2023-04-18 12:29:07 +02:00
Dennis Lichtenthäler
7b1fa90d70 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5250 of 5250 strings)

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

powered by weblate
2023-04-18 09:19:45 +02:00
Dennis Lichtenthäler
16d191c5e0 Translations: Update German
Currently translated at 100.0% (5250 of 5250 strings)

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

powered by weblate
2023-04-18 09:19:45 +02:00
Raphael Michel
1517e86034 Bump django-filter to 23.1 2023-04-17 22:08:16 +02:00
Raphael Michel
ef7c497bce Bump pypdf to 3.8.* 2023-04-17 22:07:55 +02:00
Raphael Michel
60b86d5e72 Bump django-bootstrap3 to 23.1.* 2023-04-17 22:07:40 +02:00
Raphael Michel
9329caabed Use a more precise font for displaying device tokens 2023-04-17 18:53:34 +02:00
Raphael Michel
bacd6b8191 Emails: Clean "@" in sender name 2023-04-17 10:34:24 +02:00
Raphael Michel
11e3bd4d39 Add support for GeoIP data (#3230) 2023-04-17 09:50:46 +02:00
Michael Stapelberg
c890f4cdc0 Sendmail: Add option to attach calendar invites (#3224) 2023-04-17 09:38:36 +02:00
robbi5
6aeb82b06a Add checkin requires attention to advanced order search (#3226)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-04-17 09:36:17 +02:00
Raphael Michel
e65ef392a3 Voucher creation: Search for duplicates based on upper version 2023-04-17 09:14:51 +02:00
MaartenUreel
57252dca2e Translations: Update Dutch
Currently translated at 86.1% (4525 of 5250 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
Dennis Lichtenthäler
10aa590bd3 Translations: Update German
Currently translated at 100.0% (5250 of 5250 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
Loïc Alejandro
6ce1270cbd Translations: Update French
Currently translated at 64.1% (134 of 209 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
Loïc Alejandro
5dc87d5494 Translations: Update French
Currently translated at 49.3% (2592 of 5250 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
전윤수
7ce63e5b16 Translations: Update Korean
Currently translated at 0.3% (17 of 5250 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
전윤수
27b514dc36 Translations: Update Korean
Currently translated at 0.2% (13 of 5250 strings)

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

powered by weblate
2023-04-17 09:05:22 +02:00
Raphael Michel
ca956f8e3b Add micro-optimization at import time 2023-04-16 22:57:30 +02:00
Raphael Michel
a4ad518717 Bump django-debug-toolbar to 4.0.* 2023-04-16 22:08:08 +02:00
Raphael Michel
9696fad482 Bump Pillow to 9.5.* 2023-04-16 22:08:02 +02:00
Raphael Michel
d62931d4c0 Bump beautifulsoup4 to 4.12.* 2023-04-16 22:07:52 +02:00
Raphael Michel
8212bb5875 Fix setting up webauthn with required 2FA 2023-04-16 17:24:58 +02:00
Michael Stapelberg
ba1a5f0e35 Docs: Note on periodic tasks in dev setup (#3228)
It took a little bit of searching to figure out that in the dev environment, by
default, celery tasks are run synchronously, but periodic tasks are not run at
all.
2023-04-16 14:24:35 +02:00
Raphael Michel
a2fd012106 Add-on step: Catch ValueError on invalid input 2023-04-16 14:14:29 +02:00
Raphael Michel
35a3804751 Fix AttributeError in exporters 2023-04-16 14:11:41 +02:00
Raphael Michel
a3fb10bcb0 API: Fix crash with missing body in some endpoints 2023-04-16 14:10:14 +02:00
Raphael Michel
d19cdfb83f Fix missing JavaScript in question form 2023-04-06 11:38:39 +02:00
Raphael Michel
b404f6d619 Fix crash in settings form validation 2023-04-06 10:35:29 +02:00
Raphael Michel
dda1368d81 Add option to copy shop URL and generate QR code (#3215)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-04-06 10:04:11 +02:00
Raphael Michel
ddade60625 Question: Allow limit of string length (#3214) 2023-04-06 09:58:50 +02:00
Raphael Michel
b1e8e072d4 PDF exporters: Fix ordering of add-on products by name 2023-04-06 09:34:47 +02:00
Raphael Michel
b6ade23c50 Add webhook for pretix.event.order.payment.confirmed (#3216) 2023-04-06 09:21:36 +02:00
Raphael Michel
6573578ef1 Refs #3211 -- Add code comment on ical organizer field 2023-04-05 17:56:16 +02:00
Raphael Michel
33fc752a5f PDF Editor: Set width after text 2023-04-05 17:51:34 +02:00
Raphael Michel
ff043e98f3 Bump stripe to 5.4.* 2023-04-05 14:45:41 +02:00
Raphael Michel
ddfe065ebd Bump pypdf to 3.7.* 2023-04-05 14:45:10 +02:00
Raphael Michel
bd5b63a90e Widget: Fix tests 2023-04-05 12:01:33 +02:00
Raphael Michel
0432798d23 Waiting list: Add mail placeholder for name 2023-04-05 11:28:27 +02:00
Raphael Michel
ecb2865cb8 Widget: Add hidden location in detail view 2023-04-05 11:22:40 +02:00
Raphael Michel
43a05e1cb3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-04-05 11:17:30 +02:00
Raphael Michel
fa28b9c13a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5251 of 5251 strings)

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

powered by weblate
2023-04-05 11:16:56 +02:00
Raphael Michel
8a2b38792a Translations: Update German
Currently translated at 100.0% (5251 of 5251 strings)

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

powered by weblate
2023-04-05 11:16:56 +02:00
Raphael Michel
6652c2e61a Translations: Update wordlist 2023-04-05 11:11:13 +02:00
Raphael Michel
88b3f588b8 Fix settings import 2023-04-05 11:03:03 +02:00
Raphael Michel
eabcececb0 Translations: Update wordlist 2023-04-05 11:02:37 +02:00
dependabot[bot]
31a2a40bce Bump @babel/core from 7.21.0 to 7.21.4 in /src/pretix/static/npm_dir
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.21.0 to 7.21.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.4/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-04 22:45:37 +02:00
Raphael Michel
a6e897b481 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-04-04 22:38:45 +02:00
전윤수
a000bacb1f Translations: Update Korean
Currently translated at 0.1% (10 of 5195 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
전윤수
b60d38ff3b Translations: Update Korean
Currently translated at 0.1% (2 of 5195 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Michael
dea47a4e87 Translations: Update Czech
Currently translated at 79.2% (4120 of 5197 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
전윤수
78e2ea4fef Translations: Add Korean 2023-04-04 22:37:32 +02:00
chondaen12
9173832ce1 Translations: Update Thai
Currently translated at 0.2% (11 of 5195 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Vasco Baleia
d7c9e82bae Translations: Update Portuguese (Portugal)
Currently translated at 93.4% (4855 of 5195 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Hana Happl
fb7a540efb Translations: Update Czech
Currently translated at 77.5% (4032 of 5197 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Michael
f57a9cbae0 Translations: Update Czech
Currently translated at 77.5% (4032 of 5197 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Michael
fdff1c80d3 Translations: Update Czech
Currently translated at 73.9% (3841 of 5197 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Michael
986122e7f8 Translations: Update Czech
Currently translated at 73.7% (3833 of 5197 strings)

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

powered by weblate
2023-04-04 22:37:32 +02:00
Raphael Michel
5dbb01342a Fix changing event settings 2023-04-04 19:31:03 +02:00
Raphael Michel
f81c388906 Order details: Make "view in backend" open in new tab 2023-04-04 18:33:32 +02:00
Richard Schreiber
63bc6c17c9 Fix isort 2023-04-03 13:43:33 +02:00
Richard Schreiber
5c8d1fde32 PDF: add attendee_name_parts fallback for addons (#3206) 2023-04-03 13:39:28 +02:00
Raphael Michel
d0b449ea89 Reusable media (#3131)
Co-authored-by: Martin Gross <gross@rami.io>
2023-04-03 10:45:22 +02:00
dependabot[bot]
377117548d Bump @babel/preset-env from 7.20.2 to 7.21.4 in /src/pretix/static/npm_dir (#3204)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 10:26:42 +02:00
Raphael Michel
634445b79d Check-in API: Extend reach of "force" flag (#3187) 2023-04-03 10:26:25 +02:00
Raphael Michel
496e4c800a PDF: Do not use internal names in addon list 2023-04-03 10:17:53 +02:00
Richard Schreiber
83fe53ac2b Docs: add possible values for data-consent in widget (#3198) 2023-03-31 10:35:51 +02:00
Raphael Michel
534e6eb32d Bump redis to 4.5.4+ 2023-03-30 09:20:22 +02:00
Raphael Michel
23b863df96 Voucher bulk cration: Add heuristical error message for CSV input 2023-03-29 17:55:56 +02:00
Richard Schreiber
fbca5e3ab1 Widget: fix centering for close-svg (#3192) 2023-03-29 11:04:36 +02:00
Richard Schreiber
37f6b7023c Widget: fix default-open variations being hidden (Z#23118652) 2023-03-29 10:02:47 +02:00
Raphael Michel
d5ed1b87a1 Create log entry for ordering items, categories and questions 2023-03-28 17:42:17 +02:00
Raphael Michel
2b0f754f4b Widget: improve accessibility with aria-label and role=button (#3179)
Including fixes for previous commit
2023-03-28 15:14:49 +02:00
Raphael Michel
aaebcae12b Revert "Widget: improve accessibility with aria-label and role=button (#3179)"
This reverts commit 2b479f59d5.
2023-03-28 15:03:14 +02:00
Raphael Michel
8096e958b8 Bump version to 4.19.0.dev0 2023-03-28 13:37:38 +02:00
Raphael Michel
1eb3a09240 Bump version to 4.18.0 2023-03-28 13:36:57 +02:00
Raphael Michel
b52cbf2645 PDF editor: Fix layout not being saved 2023-03-28 12:49:21 +02:00
Raphael Michel
40679bc0d8 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5195 of 5195 strings)

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

powered by weblate
2023-03-28 12:41:14 +02:00
Raphael Michel
28c04acd81 Translations: Update German
Currently translated at 100.0% (5195 of 5195 strings)

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

powered by weblate
2023-03-28 12:41:14 +02:00
Raphael Michel
0de588a566 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5195 of 5195 strings)

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

powered by weblate
2023-03-28 12:41:14 +02:00
Raphael Michel
a3bf7e37e7 Translations: Update German
Currently translated at 100.0% (5195 of 5195 strings)

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

powered by weblate
2023-03-28 12:41:14 +02:00
Michael
c91f2d9f7d Translations: Update Czech
Currently translated at 72.0% (3737 of 5186 strings)

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

powered by weblate
2023-03-28 12:41:14 +02:00
Raphael Michel
1511c6c2b4 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-03-28 10:31:33 +02:00
Raphael Michel
550826af76 Widget: Support for low availability in mobile calendar mode 2023-03-28 10:09:13 +02:00
Richard Schreiber
2b479f59d5 Widget: improve accessibility with aria-label and role=button (#3179) 2023-03-28 09:49:15 +02:00
Raphael Michel
861c689410 PDF editor: Small UX improvements (#3185) 2023-03-28 09:47:37 +02:00
Raphael Michel
c612f183ef Disable email rules if event is not live (#3181) 2023-03-28 09:23:42 +02:00
robbi5
2b482dd233 Add system question order to device event settings api endpoint (#3186) 2023-03-27 18:06:55 +02:00
Michael
3c9874b328 Translations: Update Czech
Currently translated at 71.4% (3704 of 5186 strings)

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

powered by weblate
2023-03-27 09:31:50 +02:00
Michael
342506c866 Translations: Update Czech
Currently translated at 69.8% (3624 of 5186 strings)

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

powered by weblate
2023-03-27 09:31:50 +02:00
Michael
bc84664b1f Translations: Update Czech
Currently translated at 68.2% (3540 of 5186 strings)

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

powered by weblate
2023-03-27 09:31:50 +02:00
Michael
7b8f7f48b9 Translations: Update Czech
Currently translated at 66.1% (3430 of 5186 strings)

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

powered by weblate
2023-03-27 09:31:50 +02:00
Michael
b1c252a646 Translations: Update Czech
Currently translated at 65.3% (3391 of 5186 strings)

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

powered by weblate
2023-03-27 09:31:50 +02:00
Raphael Michel
2774eb442d Bank transfer: Enable organizer-level features with multiple currencies (#3177)
Co-authored-by: Martin Gross <gross@rami.io>
2023-03-27 09:31:41 +02:00
Raphael Michel
27f0ed69d7 Promote Czeck language to inofficial 2023-03-23 09:09:12 +01:00
Raphael Michel
e1b1bb93bc Bump Stripe to 5.2.* 2023-03-22 16:01:17 +01:00
Raphael Michel
35f160929c Bump pypdf to 3.6.* 2023-03-22 16:01:17 +01:00
Raphael Michel
70507a3a14 Bump django-hijack to 3.3.* 2023-03-22 16:01:17 +01:00
Michael
79c969beab Translations: Update Czech
Currently translated at 57.8% (3001 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
46cd5428ff Translations: Update Czech
Currently translated at 56.5% (2935 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
0d4a6fc30a Translations: Update Czech
Currently translated at 55.4% (2877 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
chondaen12
51abcacf12 Translations: Update Thai
Currently translated at 0.1% (3 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Raphael Michel
88a7ef14e1 Translations: Update Czech
Currently translated at 54.6% (2833 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
81624c63b5 Translations: Update Czech
Currently translated at 54.6% (2833 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
7e070b5a3f Translations: Update Czech
Currently translated at 54.6% (2833 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
d78c2f4941 Translations: Update Czech
Currently translated at 53.0% (2749 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
c27d9b713f Translations: Update Czech
Currently translated at 53.0% (2749 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
e4107360a1 Translations: Update Czech
Currently translated at 100.0% (205 of 205 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
d9e53df6ef Translations: Update Czech
Currently translated at 46.0% (2390 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
6456a6ae65 Translations: Update Czech
Currently translated at 84.3% (173 of 205 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
624fd9e1be Translations: Update Czech
Currently translated at 39.5% (2051 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
1d85857c87 Translations: Update Czech
Currently translated at 38.6% (2005 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
8ff5f543b2 Translations: Update Czech
Currently translated at 37.3% (1936 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
ad06aa3e67 Translations: Update Czech
Currently translated at 37.3% (1936 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
dc7cbe8c46 Translations: Update Czech
Currently translated at 37.0% (1920 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
dd7f0ca0e4 Translations: Update Czech
Currently translated at 37.0% (1920 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
93d799d9b9 Translations: Update Czech
Currently translated at 84.3% (173 of 205 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
e725b384e3 Translations: Update Czech
Currently translated at 32.6% (1695 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Michael
63852ad344 Translations: Update Czech
Currently translated at 32.6% (1695 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
170399a883 Translations: Update Czech
Currently translated at 29.9% (1551 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
Hana Happl
0dd274287c Translations: Update Czech
Currently translated at 29.0% (1506 of 5186 strings)

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

powered by weblate
2023-03-22 15:59:15 +01:00
chondaen12
eebe9c7237 Translations: Add Thai 2023-03-22 15:59:15 +01:00
Richard Schreiber
d4b210c164 Widget: add role=button to close element (#3174) 2023-03-22 15:58:53 +01:00
Richard Schreiber
8a6488fd81 Order-exports: localize salutation (#3160) 2023-03-22 13:12:38 +01:00
Atlas
c52ebb4ba9 Fix minor typos & phrasing in scaling docs (#3175)
* Fix minor typos & phrasing in scaling docs

* Update scaling.rst
2023-03-21 20:03:36 +01:00
Raphael Michel
0121e053f6 Fix non-functional login page after session timeout 2023-03-21 17:19:11 +01:00
Richard Schreiber
464a25a678 Widget: fix missing voucher in seating (Z#23118206) 2023-03-21 12:21:21 +01:00
Richard Schreiber
3eceb33cfc Tests: ignore locale from pretix.cfg and default to en (#3167) 2023-03-21 08:55:34 +01:00
Richard Schreiber
e9b22b7d33 Cart: ensure free price input is decimal (PRETIXEU-80N)
Co-authored-by: Phin Wolkwitz <wolkwitz@rami.io>
2023-03-21 08:51:49 +01:00
Richard Schreiber
5ad0f92776 Widget: fix close-icon position 2023-03-21 08:46:55 +01:00
274 changed files with 196888 additions and 110266 deletions

View File

@@ -6,7 +6,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/src"
directory: "/"
schedule:
interval: "daily"
versioning-strategy: increase

View File

@@ -38,7 +38,6 @@ jobs:
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -64,7 +63,6 @@ jobs:
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -36,7 +36,6 @@ jobs:
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -57,7 +56,6 @@ jobs:
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -64,7 +64,6 @@ jobs:
run: sudo apt update && sudo apt install gettext mariadb-client
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
working-directory: ./src
- name: Run checks
run: python manage.py check
working-directory: ./src

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
env/
build/
dist/
.coverage
htmlcov/
.ropeproject

View File

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

View File

@@ -19,6 +19,8 @@ RUN apt-get update && \
python3-dev \
sudo \
supervisor \
libmaxminddb0 \
libmaxminddb-dev \
zlib1g-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
@@ -39,18 +41,6 @@ RUN apt-get update && \
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/setup.py /pretix/src/setup.py
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
@@ -58,9 +48,18 @@ 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 pyproject.toml /pretix/pyproject.toml
COPY src /pretix/src
RUN cd /pretix/src && python setup.py install
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \

47
MANIFEST.in Normal file
View File

@@ -0,0 +1,47 @@
include LICENSE
include README.rst
include src/Makefile
global-include *.proto
recursive-include src/pretix/static *
recursive-include src/pretix/static.dist *
recursive-include src/pretix/locale *
recursive-include src/pretix/helpers/locale *
recursive-include src/pretix/base/templates *
recursive-include src/pretix/control/templates *
recursive-include src/pretix/presale/templates *
recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates *
recursive-include src/pretix/plugins/manualpayment/static *
recursive-include src/pretix/plugins/paypal/templates *
recursive-include src/pretix/plugins/paypal/static *
recursive-include src/pretix/plugins/paypal2/templates *
recursive-include src/pretix/plugins/paypal2/static *
recursive-include src/pretix/plugins/src/pretixdroid/templates *
recursive-include src/pretix/plugins/src/pretixdroid/static *
recursive-include src/pretix/plugins/sendmail/templates *
recursive-include src/pretix/plugins/statistics/templates *
recursive-include src/pretix/plugins/statistics/static *
recursive-include src/pretix/plugins/stripe/templates *
recursive-include src/pretix/plugins/stripe/static *
recursive-include src/pretix/plugins/ticketoutputpdf/templates *
recursive-include src/pretix/plugins/ticketoutputpdf/static *
recursive-include src/pretix/plugins/badges/templates *
recursive-include src/pretix/plugins/badges/static *
recursive-include src/pretix/plugins/returnurl/templates *
recursive-include src/pretix/plugins/returnurl/static *
recursive-include src/pretix/plugins/webcheckin/templates *
recursive-include src/pretix/plugins/webcheckin/static *
recursive-include src *.cfg
recursive-include src *.csv
recursive-include src *.gitkeep
recursive-include src *.jpg
recursive-include src *.json
recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-exclude doc *
recursive-exclude deployment *
recursive-exclude res *

View File

@@ -481,3 +481,18 @@ You can configure the maximum file size for uploading various files::
; Max upload size for other files in MiB, defaults to 10 MiB
; This includes all file upload type order questions
max_size_other = 100
GeoIP
-----
pretix can optionally make use of a GeoIP database for some features. It needs a file in ``mmdb`` format, for example
`GeoLite2`_ or `GeoAcumen`_::
[geoip]
path=/var/geoipdata/
filename_country=GeoLite2-Country.mmdb
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data

View File

@@ -16,7 +16,7 @@ Manual installation
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles

View File

@@ -25,7 +25,7 @@ and what you should think of.
Scaling reasons
---------------
There's mainly two reasons to scale up a pretix installation beyond a single server:
There are two main reasons for scaling up a pretix installation beyond a single server:
* **Availability:** Distributing pretix over multiple servers can allow you to survive failure of one or more single machines, leading to a higher uptime and reliability of your system.
@@ -92,7 +92,7 @@ them from a different URL <config-urls>`.
pretix-web
""""""""""
The ``pretix-web`` process does not carry any internal state can be easily started on as many machines as you like, and you can
The ``pretix-web`` process does not carry any internal state and can be easily started on as many machines as you like, and you can
use the load balancing features of your frontend web server to redirect to all of them.
You can adjust the number of processes in the ``gunicorn`` command line, and we recommend choosing roughly two times the number
@@ -154,7 +154,7 @@ files, otherwise you **will** run into errors with the user interface.
The easiest solution for this is probably to store them on a NFS server that you mount
on each of the other servers.
Since we use Django's file storage mechanism internally, you can in theory also use a object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
Since we use Django's file storage mechanism internally, you can in theory also use an object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
At pretix.eu, we use a custom-built `object storage cluster`_.
@@ -171,12 +171,12 @@ you configure, so make sure to set this memory usage as high as you can afford.
memory available allows your database to make more use of caching, which is usually good.
Scaling your database to multiple machines needs to be treated with great caution. It's a
good to have a replica of your database for availability reasons. In case your primary
good idea to have a replica of your database for availability reasons. In case your primary
database server fails, you can easily switch over to the replica and continue working.
However, using database replicas for performance gains is much more complicated. When using
However, using database replicas for performance gain is much more complicated. When using
replicated database systems, you are always trading in consistency or availability to get
additional performance and the consequences of this can be subtle and it is important
additional performance and the consequences of this can be subtle. It is important
that you have a deep understanding of the semantics of your replication mechanism.
.. warning::
@@ -187,7 +187,7 @@ that you have a deep understanding of the semantics of your replication mechanis
As an example, if you buy a ticket, pretix first needs to calculate how many tickets
are left to sell. If this calculation is done on a database replica that lags behind
even for fractions of a second, the decision to allow selling the ticket will be made
on out-of-data data and you can end up with more tickets sold than configured. Similarly,
on stale data and you can end up with more tickets sold than configured. Similarly,
you could imagine situations leading to double payments etc.
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
@@ -204,9 +204,9 @@ redis
While redis is a very important part that glues together some of the components, it isn't used
heavily and can usually handle a fairly large pretix installation easily on a single modern
CPU core.
Having some memory available is good in case of e.g. lots of tasks queuing up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Having some memory available is good, e.g. if lots of tasks queue up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
Feel free to set up a redis cluster for availability but you probably won't need it for performance.
The limitations
---------------
@@ -228,9 +228,9 @@ if you add more hardware.
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
1500 orders per minute per event** in benchmarks, although even more should be possible.
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
We're working on reducing the number of cases in which this is relevant and thereby improve the possible
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -225,4 +225,3 @@ You can get three response codes:
"subevent": 23,
"checkinlist": 5
}

View File

@@ -13,6 +13,10 @@ failed scans.
The endpoints listed on this page have been added.
.. versionchanged:: 4.18
The ``source_type`` parameter has been added.
.. _`rest-checkin-redeem`:
Checking a ticket in
@@ -28,6 +32,7 @@ Checking a ticket in
passed needs to be from a distinct event.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
@@ -72,6 +77,7 @@ Checking a ticket in
{
"secret": "M5BO19XmFwAjLd4nDYUAL9ISjhti0e9q",
"source_type": "barcode",
"lists": [1],
"force": false,
"ignore_unpaid": false,
@@ -213,8 +219,8 @@ Checking a ticket in
* ``revoked`` - Ticket code has been revoked.
* ``error`` - Internal error.
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 201: no error

View File

@@ -753,8 +753,8 @@ Order position endpoints
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -547,6 +547,9 @@ Therefore, we're also not including a list of the options here, but instead reco
to see available options. The ``explain=true`` flag enables a verbose mode that provides you with human-readable
information about the properties.
Note that some settings are read-only, e.g. because they can be read on event level but currently only be changed on
organizer level.
.. note:: Please note that this is not a complete representation of all event settings. You will find more settings
in the web interface.
@@ -593,6 +596,7 @@ information about the properties.
{
"value": "https://pretix.eu",
"label": "Imprint URL",
"readonly": false,
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
}
},
@@ -606,6 +610,10 @@ information about the properties.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. versionchanged:: 4.18
The ``readonly`` flag has been added.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/settings/
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.

View File

@@ -32,6 +32,7 @@ at :ref:`plugin-docs`.
membershiptypes
memberships
giftcards
reusablemedia
carts
teams
devices

View File

@@ -108,6 +108,9 @@ generate_tickets boolean If ``false``,
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
product when it is sold out.
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
media_policy string Policy on how to handle reusable media (experimental feature).
Possible values are ``null``, ``"new"``, ``"reuse"``, and ``"reuse_or_new"``.
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used.
has_variations boolean Shows whether or not this item has variations.
@@ -189,6 +192,10 @@ meta_data object Values set fo
The ``validity_*`` attributes have been added.
.. versionchanged:: 4.18
The ``media_policy`` and ``media_type`` attributes have been added.
Notes
-----
@@ -244,6 +251,8 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -373,6 +382,8 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -483,6 +494,8 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -580,6 +593,8 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -709,6 +724,8 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,

View File

@@ -910,6 +910,7 @@ Creating orders
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``answers``
* ``question``

View File

@@ -157,6 +157,7 @@ information about the properties.
{
"value": "calendar",
"label": "Default overview style",
"readonly": false,
"help_text": "If your event series has more than 50 dates in the future, only the month or week calendar can be used."
}
},

View File

@@ -63,6 +63,7 @@ valid_date_max date Maximum value f
valid_datetime_min datetime Minimum value for date and time questions (optional)
valid_datetime_max datetime Maximum value for date and time questions (optional)
valid_file_portrait boolean Turn on file validation for portrait photos
valid_string_length_max integer Maximum length for string questions (optional)
dependency_question integer Internal ID of a different question. The current
question will only be shown if the question given in
this attribute is set to the value given in
@@ -122,6 +123,7 @@ Endpoints
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"dependency_question": null,
"dependency_value": null,
@@ -201,6 +203,7 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -302,6 +305,7 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"options": [
{
"id": 1,
@@ -384,6 +388,7 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"options": [
{
"id": 1,

View File

@@ -0,0 +1,317 @@
.. _`rest-reusablemedia`:
Reusable media
==============
Reusable media represent things, typically physical tokens like plastic cards or NFC wristbands, which can represent
other entities inside the system. For example, a medium can link to an order position or to a gift card and can be used
in their place. Later, the medium might be reused for a different ticket.
Resource description
--------------------
The reusable medium resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used.
created datetime Date of creation
updated datetime Date of last modification
expires datetime Expiry date (or ``null``)
customer string Identifier of a customer account this medium belongs to.
linked_orderposition integer Internal ID of a ticket this medium is linked to.
linked_giftcard integer Internal ID of a gift card this medium is linked to.
info object Additional data, content depends on the ``type``. Consider
this internal to the system and don't use it for your own data.
notes string Internal notes and comments (or ``null``)
===================================== ========================== =======================================================
Existing media types are:
- ``barcode``
- ``nfc_uid``
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/reusablemedia/
Returns a list of all media issued by a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/reusablemedia/ 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/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1.
:query string identifier: Only show media with the given identifier. Note that you should use the lookup endpoint described below for most use cases.
:query string type: Only show media with the given type.
:query boolean active: Only show media that are (not) active.
:query string customer: Only show media linked to the given customer.
:query string created_since: Only show media created since a given date.
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/reusablemedia/(id)/
Returns information on one medium, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/reusablemedia/1/ 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/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the medium to fetch
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/reusablemedia/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "ABCDEFGH",
"type": "barcode",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to look up a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
:statuscode 400: The medium could not be looked up due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/
Creates a new reusable medium.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/reusablemedia/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "ABCDEFGH",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to create a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
:statuscode 400: The medium could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/reusablemedia/(id)/
Update a reusable medium. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id``, ``identifier`` and ``type`` fields.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/reusablemedia/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"linked_orderposition": 13
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": 13,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the medium to modify
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 200: no error
:statuscode 400: The medium could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.

View File

@@ -26,6 +26,7 @@ can_create_events boolean
can_change_teams boolean
can_change_organizer_settings boolean
can_manage_customers boolean
can_manage_reusable_media boolean
can_manage_gift_cards boolean
can_change_event_settings boolean
can_change_items boolean
@@ -36,6 +37,10 @@ can_change_vouchers boolean
can_checkin_orders boolean
===================================== ========================== =======================================================
.. versionchanged:: 4.18
The ``can_manage_reusable_media`` permission has been added.
Team member resource
--------------------

View File

@@ -47,6 +47,7 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.refund.done``
* ``pretix.event.order.refund.canceled``
* ``pretix.event.order.refund.failed``
* ``pretix.event.order.payment.confirmed``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.checkin``

View File

@@ -58,11 +58,11 @@ If you do not have a recent installation of ``nodejs``, install it now::
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
cd src/
pip3 install -e ".[dev]"
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
cd src/
python manage.py collectstatic --noinput
Then, create the local database::
@@ -150,6 +150,13 @@ Add this to your ``src/pretix.cfg``::
Then execute ``python -m smtpd -n -c DebuggingServer localhost:1025``.
Working with periodic tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Periodic tasks (like sendmail rules) are run when an external scheduler (like cron)
triggers the ``runperiodic`` command.
To run periodic tasks, execute ``python manage.py runperiodic``.
Working with translations
^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to translate new strings that are not yet known to the translation system,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -38,27 +38,27 @@ else
endif
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
-right->[no] "Return error CANCELED"
-right->[no && !force] "Return error CANCELED"
else
-down->[yes] "Is one or more block set on the ticket?"
-down->[yes || force] "Is one or more block set on the ticket?"
--> if "" then
-right->[no] "Return error BLOCKED"
-right->[no && !force] "Return error BLOCKED"
else
-down->[yes] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
--> if "" then
-right->[no] "Return error INVALID_TIME"
-right->[no && !force] "Return error INVALID_TIME"
else
-down->[yes] "Is the product part of the check-in list?"
-down->[yes || force] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT"
-right->[no && !force] "Return error PRODUCT"
else
-down->[yes] "Is the subevent part of the check-in list?"
-down->[yes || force] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT "
-right->[no && !force] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID\nor is this a forced upload?"
-down->[yes] "Is the order in status PAID?"
--> if "" then
-right->[no] "Is Order.require_approval set?"
-right->[no && !force] "Is Order.require_approval set?"
--> if "" then
-->[no] "Is Order.valid_if_pending set?"
--> if "" then
@@ -80,7 +80,7 @@ else
-->[yes] "Return error UNPAID "
endif
else
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif

View File

@@ -1,10 +1,10 @@
sphinx==6.1.*
sphinx==6.2.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==7.*
sphinxcontrib-spelling==8.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*

View File

@@ -1,11 +1,11 @@
-e ../src/
sphinx==6.1.*
-e ../
sphinx==6.2.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==7.*
sphinxcontrib-spelling==8.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*

View File

@@ -318,7 +318,10 @@ Currently, the following attributes are understood by pretix itself:
* If ``data-consent="…"`` is given, the cookie consent mechanism will be initialized with consent for the given cookie
providers. All other providers will be disabled, no consent dialog will be shown. This is useful if you already
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
When using the pretix-tracking plugin, the following values are supported::
``adform, facebook, gosquared, google_ads, google_analytics, hubspot, linkedin, matomo, twitter``
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:

185
pyproject.toml Normal file
View File

@@ -0,0 +1,185 @@
[project]
name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.9"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
{name = "pretix team", email = "support@pretix.eu"},
]
maintainers = [
{name = "pretix team", email = "support@pretix.eu"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Other Audience",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 3.2",
]
dependencies = [
# Note that many of these are repeated as build-time dependencies down below -- change them too in case of updates!
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.2.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==23.1",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.4",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*",
"django-otp==1.1.*",
"django-phonenumber-field==7.0.*",
"django-redis==5.2.*",
"django-scopes==1.2.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.2.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.2.*",
"libsass==0.22.*",
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.23.*",
"oauthlib==3.2.*",
"openpyxl==3.1.*",
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.6.*",
"phonenumberslite==8.13.*",
"Pillow==9.5.*",
"protobuf==4.22.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.17.*",
"pypdf==3.8.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"reportlab==3.6.*",
"requests==2.28.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
"stripe==5.4.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==0.4.*",
"zeep==4.2.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
mysql = ["mysqlclient"]
dev = [
"coverage",
"coveralls",
"django-debug-toolbar==4.0.*",
"django-formset-js-improved==0.5.0.3",
"django-oauth-toolkit==2.2.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
"oauthlib==3.2.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
"pyflakes==3.0.*",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.2.*",
"pytest==7.3.*",
"responses",
]
[project.entry-points."distutils.commands"]
build = "pretix._build:CustomBuild"
build_ext = "pretix._build:CustomBuildExt"
[build-system]
requires = [
"setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata",
# These are runtime dependencies that we unfortunately need to be import in the step that generates
# all CSS and JS asset files. We should keep their versions in sync with the definition above.
"babel",
"Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-formtools==2.4",
"django-hierarkey==1.1.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-phonenumber-field==7.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"libsass==0.22.*",
"phonenumberslite==8.13.*",
"pycountry",
"pyuca",
"slimit",
]
[project.urls]
homepage = "https://pretix.eu"
documentation = "https://docs.pretix.eu"
repository = "https://github.com/pretix/pretix.git"
changelog = "https://pretix.eu/about/en/blog/"
[tool.setuptools]
include-package-data = true
[tool.setuptools.dynamic]
version = {attr = "pretix.__version__"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["pretix*"]
namespaces = false

40
setup.cfg Normal file
View File

@@ -0,0 +1,40 @@
[check-manifest]
ignore =
env/**
doc/*
deployment/*
res/*
src/.update-locales
src/Makefile
src/manage.py
src/pretix/icons/*
src/pretix/static.dist/**
src/pretix/static/jsi18n/**
src/requirements.txt
src/requirements/*
src/tests/*
src/tests/api/*
src/tests/base/*
src/tests/control/*
src/tests/testdummy/*
src/tests/templates/*
src/tests/presale/*
src/tests/doc/*
src/tests/helpers/*
src/tests/media/*
src/tests/multidomain/*
src/tests/plugins/*
src/tests/plugins/badges/*
src/tests/plugins/banktransfer/*
src/tests/plugins/paypal/*
src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/*
src/tests/plugins/sendmail/*
src/tests/plugins/ticketoutputpdf/*
.*
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
SECURITY.md

26
setup.py Normal file
View File

@@ -0,0 +1,26 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import setuptools
if __name__ == "__main__":
setuptools.setup()

View File

@@ -1,33 +0,0 @@
include LICENSE
include README.rst
global-include *.proto
recursive-include pretix/static *
recursive-include pretix/static.dist *
recursive-include pretix/locale *
recursive-include pretix/helpers/locale *
recursive-include pretix/base/templates *
recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *
recursive-include pretix/plugins/banktransfer/templates *
recursive-include pretix/plugins/banktransfer/static *
recursive-include pretix/plugins/manualpayment/templates *
recursive-include pretix/plugins/manualpayment/static *
recursive-include pretix/plugins/paypal/templates *
recursive-include pretix/plugins/paypal/static *
recursive-include pretix/plugins/paypal2/templates *
recursive-include pretix/plugins/paypal2/static *
recursive-include pretix/plugins/pretixdroid/templates *
recursive-include pretix/plugins/pretixdroid/static *
recursive-include pretix/plugins/sendmail/templates *
recursive-include pretix/plugins/statistics/templates *
recursive-include pretix/plugins/statistics/static *
recursive-include pretix/plugins/stripe/templates *
recursive-include pretix/plugins/stripe/static *
recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
recursive-include pretix/plugins/returnurl/templates *
recursive-include pretix/plugins/returnurl/static *
recursive-include pretix/plugins/webcheckin/templates *
recursive-include pretix/plugins/webcheckin/static *

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

View File

@@ -0,0 +1,251 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import os
import django.conf.locale
from pycountry import currencies
from django.utils.translation import gettext_lazy as _ # NOQA
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
USE_I18N = True
USE_L10N = True
USE_TZ = True
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'pretix.base',
'pretix.control',
'pretix.presale',
'pretix.multidomain',
'pretix.api',
'pretix.helpers',
'rest_framework',
'djangoformsetjs',
'compressor',
'bootstrap3',
'pretix.plugins.banktransfer',
'pretix.plugins.stripe',
'pretix.plugins.paypal',
'pretix.plugins.paypal2',
'pretix.plugins.ticketoutputpdf',
'pretix.plugins.sendmail',
'pretix.plugins.statistics',
'pretix.plugins.reports',
'pretix.plugins.checkinlists',
'pretix.plugins.pretixdroid',
'pretix.plugins.badges',
'pretix.plugins.manualpayment',
'pretix.plugins.returnurl',
'pretix.plugins.webcheckin',
'django_countries',
'oauth2_provider',
'phonenumber_field',
'statici18n',
]
FORMAT_MODULE_PATH = [
'pretix.helpers.formats',
]
ALL_LANGUAGES = [
('en', _('English')),
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
('nl-informal', _('Dutch (informal)')),
('fr', _('French')),
('fi', _('Finnish')),
('gl', _('Galician')),
('el', _('Greek')),
('it', _('Italian')),
('lv', _('Latvian')),
('pl', _('Polish')),
('pt-pt', _('Portuguese (Portugal)')),
('pt-br', _('Portuguese (Brazil)')),
('ro', _('Romanian')),
('ru', _('Russian')),
('es', _('Spanish')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
LANGUAGES_OFFICIAL = {
'en', 'de', 'de-informal'
}
LANGUAGES_RTL = {
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'pl', 'fi', 'pt-br', 'gl',
}
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
]
EXTRA_LANG_INFO = {
'de-informal': {
'bidi': False,
'code': 'de-informal',
'name': 'German (informal)',
'name_local': 'Deutsch',
'public_code': 'de',
},
'nl-informal': {
'bidi': False,
'code': 'nl-informal',
'name': 'Dutch (informal)',
'name_local': 'Nederlands',
'public_code': 'nl',
},
'fr': {
'bidi': False,
'code': 'fr',
'name': 'French',
'name_local': 'Français'
},
'lv': {
'bidi': False,
'code': 'lv',
'name': 'Latvian',
'name_local': 'Latviešu'
},
'pt-pt': {
'bidi': False,
'code': 'pt-pt',
'name': 'Portuguese',
'name_local': 'Português',
},
}
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
template_loaders = (
'django.template.loaders.filesystem.Loader',
'pretix.helpers.template_loaders.AppLoader',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
"django.template.context_processors.request",
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'pretix.base.context.contextprocessor',
'pretix.control.context.contextprocessor',
'pretix.presale.context.contextprocessor',
],
'loaders': template_loaders
},
},
]
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'pretix/static')
] if os.path.exists(os.path.join(BASE_DIR, 'pretix/static')) else []
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
('text/vue', 'pretix.helpers.compressor.VueCompiler'),
)
COMPRESS_OFFLINE_CONTEXT = {
'basetpl': 'empty.html',
}
COMPRESS_ENABLED = True
COMPRESS_OFFLINE = True
COMPRESS_FILTERS = {
'css': (
# CssAbsoluteFilter is incredibly slow, especially when dealing with our _flags.scss
# However, we don't need it if we consequently use the static() function in Sass
# 'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
),
'js': (
'compressor.filters.jsmin.JSMinFilter',
)
}
CURRENCIES = list(currencies)
CURRENCY_PLACES = {
# default is 2
'BIF': 0,
'CLP': 0,
'DJF': 0,
'GNF': 0,
'JPY': 0,
'KMF': 0,
'KRW': 0,
'MGA': 0,
'PYG': 0,
'RWF': 0,
'VND': 0,
'VUV': 0,
'XAF': 0,
'XOF': 0,
'XPF': 0,
}
PRETIX_EMAIL_NONE_VALUE = 'none@well-known.pretix.eu'
PRETIX_PRIMARY_COLOR = '#8E44B3'
# pretix includes caching options for some special situations where full HTML responses are cached. This might be
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
CACHE_LARGE_VALUES_ALLOWED = False
CACHE_LARGE_VALUES_ALIAS = 'default'

74
src/pretix/_build.py Normal file
View File

@@ -0,0 +1,74 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import os
import shutil
import subprocess
from setuptools.command.build import build
from setuptools.command.build_ext import build_ext
here = os.path.abspath(os.path.dirname(__file__))
npm_installed = False
def npm_install():
global npm_installed
if not npm_installed:
# keep this in sync with Makefile!
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
os.makedirs(node_prefix, exist_ok=True)
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
npm_installed = True
class CustomBuild(build):
def run(self):
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
os.environ.setdefault("PRETIX_IGNORE_CONFLICTS", "True")
import django
django.setup()
from django.conf import settings
from django.core import management
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
npm_install()
management.call_command('compilemessages', verbosity=1)
management.call_command('compilejsi18n', verbosity=1)
management.call_command('collectstatic', verbosity=1, interactive=False)
management.call_command('compress', verbosity=1)
build.run(self)
class CustomBuildExt(build_ext):
def run(self):
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
npm_install()
build_ext.run(self)

View File

@@ -0,0 +1,48 @@
#
# 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/>.
#
"""
This file contains settings that we need at wheel require time. All settings that we only need at runtime are set
in settings.py.
"""
from ._base_settings import * # NOQA
ENTROPY = {
'order_code': 5,
'customer_identifier': 7,
'ticket_secret': 32,
'voucher_code': 16,
'giftcard_secret': 12,
}
MAIL_FROM_ORGANIZERS = 'invalid@invalid'
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
DEFAULT_CURRENCY = 'EUR'
SECRET_KEY = "build-time-secret-key"
HAS_REDIS = False
STATIC_URL = '/static/'
HAS_MEMCACHED = False
HAS_CELERY = False
HAS_GEOIP = False
SENTRY_ENABLED = False

View File

@@ -81,6 +81,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'),
)
@@ -220,6 +221,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
)

View File

@@ -26,6 +26,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList
@@ -84,6 +85,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
secret = serializers.CharField(required=True, allow_null=False)
force = serializers.BooleanField(default=False, required=False)
source_type = serializers.ChoiceField(choices=[(k, v) for k, v in MEDIA_TYPES.items()], default='barcode')
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
ignore_unpaid = serializers.BooleanField(default=False, required=False)
questions_supported = serializers.BooleanField(default=True, required=False)

View File

@@ -797,6 +797,21 @@ class EventSettingsSerializer(SettingsSerializer):
'logo_show_title',
'og_image',
'name_scheme',
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):
@@ -863,6 +878,9 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
'name_scheme',
'reusable_media_type_barcode',
'reusable_media_type_nfc_uid',
'system_question_order',
]
def __init__(self, *args, **kwargs):

View File

@@ -244,7 +244,8 @@ class ItemSerializer(I18nAwareModelSerializer):
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',
'validity_dynamic_duration_minutes', 'validity_dynamic_duration_hours', 'validity_dynamic_duration_days',
'validity_dynamic_duration_months', 'validity_dynamic_start_choice', 'validity_dynamic_start_choice_day_limit')
'validity_dynamic_duration_months', 'validity_dynamic_start_choice', 'validity_dynamic_start_choice_day_limit',
'media_policy', 'media_type')
read_only_fields = ('has_variations',)
def __init__(self, *args, **kwargs):
@@ -263,6 +264,7 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
Item.clean_media_settings(self.context['event'], data.get('media_policy'), data.get('media_type'), data.get('issue_giftcard'))
if data.get('personalized') and not data.get('admission'):
raise ValidationError(_('Only admission products can currently be personalized.'))
@@ -440,7 +442,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
'valid_file_portrait')
'valid_string_length_max', 'valid_file_portrait')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -0,0 +1,128 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
logger = logging.getLogger(__name__)
class NestedOrderMiniSerializer(I18nAwareModelSerializer):
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
class Meta:
model = Order
fields = ['code', 'event']
class NestedOrderPositionSerializer(OrderPositionSerializer):
order = NestedOrderMiniSerializer()
class NestedGiftCardSerializer(GiftCardSerializer):
def to_representation(self, instance):
d = super().to_representation(instance)
if hasattr(instance, 'cached_value'):
d['value'] = str(Decimal(instance.cached_value).quantize(Decimal("0.01")))
else:
d['value'] = str(Decimal(instance.value).quantize(Decimal("0.01")))
return d
class ReusableMediaSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=self.context['organizer'].issued_gift_cards.all()
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'customer' in self.context['request'].query_params.getlist('expand'):
self.fields['customer'] = CustomerSerializer(read_only=True)
else:
self.fields['customer'] = serializers.SlugRelatedField(
required=False,
allow_null=True,
slug_field='identifier',
queryset=self.context['organizer'].customers.all()
)
def validate(self, data):
data = super().validate(data)
if 'type' in data and 'identifier' in data:
qs = self.context['organizer'].reusable_media.filter(
identifier=data['identifier'], type=data['type']
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(
{'identifier': _('A medium with the same identifier and type already exists in your organizer account.')}
)
return data
class Meta:
model = ReusableMedium
fields = (
'id',
'created',
'updated',
'type',
'identifier',
'active',
'expires',
'customer',
'linked_orderposition',
'linked_giftcard',
'info',
'notes',
)
class MediaLookupInputSerializer(serializers.Serializer):
type = serializers.CharField(required=True)
identifier = serializers.CharField(required=True)

View File

@@ -33,6 +33,7 @@ from django.utils.encoding import force_str
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
from django_countries.fields import Country
from django_scopes import scopes_disabled
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
@@ -48,8 +49,8 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
SubEvent, TaxRule, Voucher,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -356,6 +357,9 @@ class PdfDataSerializer(serializers.Field):
def to_representation(self, instance: OrderPosition):
res = {}
if 'event' not in self.context:
return {}
ev = instance.subevent or instance.order.event
with language(instance.order.locale, instance.order.event.settings.region):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
@@ -784,13 +788,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
required=False, allow_null=True)
country = CompatibleCountryField(source='*')
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from')
'requested_valid_from', 'use_reusable_medium')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -799,6 +805,9 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
v.required = False
v.allow_blank = True
v.allow_null = True
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -807,6 +816,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return secret
def validate_use_reusable_medium(self, m):
if m.organizer_id != self.context['event'].organizer_id:
raise ValidationError(
'The specified medium does not belong to this organizer.'
)
return m
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
@@ -1264,7 +1280,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas'})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
if simulate:
pos.order = order._wrapped
else:
@@ -1332,6 +1348,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
# Save instances
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax()
@@ -1370,6 +1387,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:
if cp.addon_to_id:

View File

@@ -183,7 +183,7 @@ class TeamSerializer(serializers.ModelSerializer):
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers'
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
)
def validate(self, data):
@@ -333,6 +333,12 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):

View File

@@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
class SettingsSerializer(serializers.Serializer):
default_fields = []
readonly_fields = []
def __init__(self, *args, **kwargs):
self.changed_data = []
@@ -59,8 +60,13 @@ class SettingsSerializer(serializers.Serializer):
f.parent = self
self.fields[fname] = f
def validate(self, attrs):
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if attr in self.readonly_fields:
continue
if isinstance(value, FieldFile):
# Delete old file
fname = instance.get(attr, as_type=File)

View File

@@ -42,9 +42,9 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, device, discount, event, exporters, idempotency, item, oauth,
order, organizer, shredders, upload, user, version, voucher, waitinglist,
webhooks,
checkin, device, discount, event, exporters, idempotency, item, media,
oauth, order, organizer, shredders, upload, user, version, voucher,
waitinglist, webhooks,
)
router = routers.DefaultRouter()
@@ -59,6 +59,7 @@ orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'customers', organizer.CustomerViewSet)
orga_router.register(r'memberships', organizer.MembershipViewSet)
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')

View File

@@ -59,7 +59,7 @@ from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, RevokedTicketSecret, TeamAPIToken,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
@@ -396,7 +396,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
legacy_url_support=False):
source_type='barcode', legacy_url_support=False):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -422,6 +422,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
common_checkin_args = dict(
raw_barcode=raw_barcode,
raw_source_type=source_type,
type=checkin_type,
list=checkinlists[0],
datetime=datetime,
@@ -433,7 +434,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_barcode_for_checkin = None
from_revoked_secret = False
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
# parent secret
queryset = _checkin_list_position_queryset(checkinlists, pdf_data=pdf_data, ignore_status=True, ignore_products=True).order_by(
F('addon_to').asc(nulls_first=True)
@@ -457,98 +458,111 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
# might be a revoked one that we actually know (-> error, but with better error message and logging and
# with respecting the force option).
# with respecting the force option), or it's a reusable medium (-> proceed with that)
if not op_candidates:
revoked_matches = list(RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
try:
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
organizer_id=checkinlists[0].event.organizer_id,
type=source_type,
identifier=raw_barcode,
linked_orderposition__isnull=False,
)
raw_barcode_for_checkin = raw_barcode
except ReusableMedium.DoesNotExist:
revoked_matches = list(
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
try:
parsed = s.parse_secret(raw_barcode)
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 legacy_url_support and isinstance(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:
parsed = s.parse_secret(raw_barcode)
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
brand = auth.software_brand
ver = parse(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
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(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 = auth.software_brand
ver = parse(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,
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
op_candidates = [revoked_matches[0].position]
if list_by_event[revoked_matches[0].event_id].addon_match:
op_candidates += list(revoked_matches[0].position.addons.all())
raw_barcode_for_checkin = raw_barcode
from_revoked_secret = True
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
op_candidates = [revoked_matches[0].position]
if list_by_event[revoked_matches[0].event_id].addon_match:
op_candidates += list(revoked_matches[0].position.addons.all())
raw_barcode_for_checkin = raw_barcode_for_checkin or raw_barcode
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
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=_make_context(context, revoked_matches[
0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
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=_make_context(context, revoked_matches[0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
op_candidates = [media.linked_orderposition] + list(media.linked_orderposition.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
@@ -634,6 +648,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
auth=auth,
type=checkin_type,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
)
except RequiredQuestionsError as e:
@@ -812,6 +827,7 @@ class CheckinRPCRedeemView(views.APIView):
return _redeem_process(
checkinlists=s.validated_data['lists'],
raw_barcode=s.validated_data['secret'],
source_type=s.validated_data['source_type'],
answers_data=s.validated_data.get('answers'),
datetime=s.validated_data.get('datetime') or now(),
force=s.validated_data['force'],

View File

@@ -542,7 +542,8 @@ class EventSettingsView(views.APIView):
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None)
'help_text': getattr(field, '_help_text', None),
'readonly': fname in s.readonly_fields,
} for fname, field in s.fields.items()
})
return Response(s.data)

View File

@@ -0,0 +1,160 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
import django_filters
from django.db import transaction
from django.db.models import OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.media import (
MediaLookupInputSerializer, ReusableMediaSerializer,
)
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class ReusableMediumFilter(FilterSet):
identifier = django_filters.CharFilter(field_name='identifier')
type = django_filters.CharFilter(field_name='type')
customer = django_filters.CharFilter(field_name='customer__identifier')
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
class Meta:
model = ReusableMedium
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
class ReusableMediaViewSet(viewsets.ModelViewSet):
serializer_class = ReusableMediaSerializer
queryset = ReusableMedium.objects.none()
permission = 'can_manage_reusable_media'
write_permission = 'can_manage_reusable_media'
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('-updated', '-id')
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
filterset_class = ReusableMediumFilter
def get_queryset(self):
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk')
).order_by().values('card').annotate(s=Sum('value')).values('s')
return self.request.organizer.reusable_media.prefetch_related(
Prefetch(
'linked_orderposition',
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
)
),
Prefetch(
'linked_giftcard',
queryset=GiftCard.objects.annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
)
)
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
inst.log_action(
'pretix.reusable_medium.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
inst.log_action(
'pretix.reusable_medium.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
def perform_destroy(self, instance):
raise MethodNotAllowed("Media cannot be deleted.")
@action(methods=["POST"], detail=False)
def lookup(self, request, *args, **kwargs):
s = MediaLookupInputSerializer(
data=request.data,
)
s.is_valid(raise_exception=True)
try:
m = ReusableMedium.objects.get(
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
organizer=request.organizer,
)
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
return Response({"result": None})
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now())
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})

View File

@@ -67,8 +67,8 @@ from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
Invoice, InvoiceAddress, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition,
OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken,
generate_secret,
OrderRefund, Quota, ReusableMedium, SubEvent, SubEventMetaValue, TaxRule,
TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import (
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
@@ -148,9 +148,13 @@ with scopes_disabled():
else:
code = Q(code__icontains=Order.normalize_code(u))
invoice_nos = {u, u.upper()}
if u.isdigit():
for i in range(2, 12):
invoice_nos.add(u.zfill(i))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
Q(invoice_no__in=invoice_nos)
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
@@ -162,12 +166,15 @@ with scopes_disabled():
)
).values('id')
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
mainq = (
code
| Q(email__icontains=u)
| Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(pk__in=matching_media)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
@@ -244,7 +251,8 @@ class OrderViewSet(viewsets.ModelViewSet):
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
Prefetch('addons', opq.select_related('item', 'variation', 'seat')),
'linked_media',
).select_related('seat', 'addon_to', 'addon_to__seat')
)
else:
@@ -313,7 +321,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
order = self.get_object()
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
@@ -372,7 +380,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
comment = request.data.get('comment', None)
cancellation_fee = request.data.get('cancellation_fee', None)
if cancellation_fee:
@@ -431,7 +439,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
order = self.get_object()
try:
@@ -449,7 +457,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def deny(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
comment = request.data.get('comment', '')
order = self.get_object()
@@ -639,13 +647,11 @@ class OrderViewSet(viewsets.ModelViewSet):
raise ValidationError(_('One of the selected products is not available in the selected country.'))
send_mail = serializer._send_mail
order = serializer.instance
if not order.pk:
# Simulation
# Simulation -- exit here
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
prefetch_related_objects([order], self._positions_prefetch(request))
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
@@ -679,6 +685,10 @@ class OrderViewSet(viewsets.ModelViewSet):
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
# Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request))
serializer = OrderSerializer(order, context=serializer.context)
if send_mail:
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
@@ -917,6 +927,7 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
@@ -926,6 +937,7 @@ with scopes_disabled():
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
| Q(pk__in=matching_media)
)
def has_checkin_qs(self, queryset, name, value):
@@ -1005,6 +1017,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
'linked_media',
Prefetch('order', self.request.event.orders.select_related('invoice_address').prefetch_related(
Prefetch(
'positions',
@@ -1440,7 +1453,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return order.payments.all()
def create(self, request, *args, **kwargs):
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
@@ -1485,7 +1498,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
send_mail = request.data.get('send_email', True)
send_mail = request.data.get('send_email', True) if request.data else True
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -457,7 +457,8 @@ class OrganizerSettingsView(views.APIView):
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None)
'help_text': getattr(field, '_help_text', None),
'readonly': fname in s.readonly_fields,
} for fname, field in s.fields.items()
})
return Response(s.data)

View File

@@ -256,6 +256,10 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.refund.failed',
_('Refund of payment failed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.payment.confirmed',
_('Payment confirmed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.approved',
_('Order approved'),

View File

@@ -35,16 +35,14 @@ from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
@@ -663,6 +661,11 @@ def base_placeholders(sender, **kwargs):
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
@@ -672,6 +675,10 @@ def base_placeholders(sender, **kwargs):
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
@@ -693,10 +700,3 @@ def base_placeholders(sender, **kwargs):
))
return ph
def get_name_parts_localized(name_parts, key):
value = name_parts.get(key, "")
if key == "salutation":
return pgettext_lazy("person_name_salutation", value)
return value

View File

@@ -94,7 +94,7 @@ class EventDataExporter(ListExporter):
]
def get_filename(self):
return '{}_events'.format(self.events.first().organizer.slug)
return '{}_events'.format(self.organizer.slug)
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_eventdata")

View File

@@ -155,7 +155,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
self.progress_callback(counter / total * 100)
if self.is_multievent:
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
filename = '{}_invoices.zip'.format(self.organizer.slug)
else:
filename = '{}_invoices.zip'.format(self.event.slug)
@@ -415,7 +415,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_invoices'.format(self.events.first().organizer.slug)
return '{}_invoices'.format(self.organizer.slug)
else:
return '{}_invoices'.format(self.event.slug)

View File

@@ -219,7 +219,7 @@ class ItemDataExporter(ListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_products'.format(self.events.first().organizer.slug)
return '{}_products'.format(self.organizer.slug)
return '{}_products'.format(self.event.slug)
def prepare_xlsx_sheet(self, ws):

View File

@@ -63,7 +63,7 @@ class MailExporter(BaseExporter):
| set(a['attendee_email'] for a in pos if a['attendee_email']))
if self.is_multievent:
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
return '{}_pretixemails.txt'.format(self.organizer.slug), 'text/plain', data.encode("utf-8")
else:
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")

View File

@@ -56,7 +56,7 @@ from pretix.base.models import (
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
@@ -340,7 +340,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
get_name_parts_localized(order.invoice_address.name_parts, k)
)
row += [
order.invoice_address.street,
@@ -477,7 +477,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
get_name_parts_localized(order.invoice_address.name_parts, k)
)
row += [
order.invoice_address.street,
@@ -660,7 +660,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
op.attendee_name_parts.get(k, '')
get_name_parts_localized(op.attendee_name_parts, k)
)
row += [
op.attendee_email,
@@ -688,8 +688,8 @@ class OrderListExporter(MultiSheetListExporter):
row += [
_('Yes') if op.blocked else '',
date_format(op.valid_from, 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
date_format(op.valid_until, 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
date_format(op.valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
date_format(op.valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
]
row.append(order.comment)
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
@@ -721,7 +721,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
get_name_parts_localized(order.invoice_address.name_parts, k)
)
row += [
order.invoice_address.street,
@@ -754,7 +754,7 @@ class OrderListExporter(MultiSheetListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_orders'.format(self.events.first().organizer.slug)
return '{}_orders'.format(self.organizer.slug)
else:
return '{}_orders'.format(self.event.slug)
@@ -880,7 +880,7 @@ class PaymentListExporter(ListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_payments'.format(self.events.first().organizer.slug)
return '{}_payments'.format(self.organizer.slug)
else:
return '{}_payments'.format(self.event.slug)
@@ -1037,7 +1037,7 @@ class GiftcardRedemptionListExporter(ListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
return '{}_giftcardredemptions'.format(self.organizer.slug)
else:
return '{}_giftcardredemptions'.format(self.event.slug)

View File

@@ -45,6 +45,7 @@ import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -91,6 +92,7 @@ from pretix.helpers.countries import (
CachedCountries, get_phone_prefixes_sorted_and_localized,
)
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -351,6 +353,15 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
return ""
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
return guess_country(event)
def guess_country(event):
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
@@ -382,6 +393,12 @@ def guess_phone_prefix(event):
return get_phone_prefix(country)
def guess_phone_prefix_from_request(request, event):
with language(get_babel_locale()):
country = str(guess_country_from_request(request, event))
return get_phone_prefix(country)
def get_phone_prefix(country):
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if country in values:
@@ -564,6 +581,7 @@ class BaseQuestionsForm(forms.Form):
:param cartpos: The cart position the form should be for
:param event: The event this belongs to
"""
request = kwargs.pop('request', None)
cartpos = self.cartpos = kwargs.pop('cartpos', None)
orderpos = self.orderpos = kwargs.pop('orderpos', None)
pos = cartpos or orderpos
@@ -661,7 +679,7 @@ class BaseQuestionsForm(forms.Form):
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
country = (cartpos.country if cartpos else orderpos.country) or guess_country_from_request(request, event)
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
@@ -747,12 +765,14 @@ class BaseQuestionsForm(forms.Form):
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=label, required=required,
max_length=q.valid_string_length_max,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_TEXT:
field = forms.CharField(
label=label, required=required,
max_length=q.valid_string_length_max,
help_text=help_text,
widget=forms.Textarea,
initial=initial.answer if initial else None,
@@ -766,7 +786,7 @@ class BaseQuestionsForm(forms.Form):
help_text=help_text,
widget=forms.Select,
empty_label=' ',
initial=initial.answer if initial else (guess_country(event) if required else None),
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
@@ -854,7 +874,7 @@ class BaseQuestionsForm(forms.Form):
initial = None
if not initial:
phone_prefix = guess_phone_prefix(event)
phone_prefix = guess_phone_prefix_from_request(request, event)
if phone_prefix:
initial = "+{}.".format(phone_prefix)
@@ -990,7 +1010,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
kwargs['initial']['country'] = guess_country(self.event)
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:

View File

@@ -49,6 +49,7 @@ class Command(BaseCommand):
except ImportError:
cmd = 'shell'
del options['skip_checks']
del options['print_sql']
if options['print_sql']:
connection.force_debug_cursor = True

116
src/pretix/base/media.py Normal file
View File

@@ -0,0 +1,116 @@
#
# 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 django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
class BaseMediaType:
medium_created_by_server = False
supports_orderposition = False
supports_giftcard = False
@property
def identifier(self):
raise NotImplementedError()
@property
def verbose_name(self):
raise NotImplementedError()
def generate_identifier(self, organizer):
if self.medium_created_by_server:
raise NotImplementedError()
else:
raise ValueError("Media type does not allow to generate identifier")
def is_active(self, organizer):
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
def handle_unknown(self, organizer, identifier, user, auth):
pass
def __str__(self):
return str(self.verbose_name)
class BarcodePlainMediaType(BaseMediaType):
identifier = 'barcode'
verbose_name = _('Barcode / QR-Code')
medium_created_by_server = True
supports_giftcard = False
supports_orderposition = True
def generate_identifier(self, organizer):
return get_random_string(
length=organizer.settings.reusable_media_type_barcode_identifier_length,
# Exclude o,0,1,i to avoid confusion with bad fonts/printers
# We use upper case to make collisions with ticket secrets less likely
allowed_chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
)
class NfcUidMediaType(BaseMediaType):
identifier = 'nfc_uid'
verbose_name = _('NFC UID-based')
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
def handle_unknown(self, organizer, identifier, user, auth):
from pretix.base.models import GiftCard, ReusableMedium
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
if identifier.startswith("08"):
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
# UIDs on every read, so they won't be useful.
return
with transaction.atomic():
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
m = ReusableMedium.objects.create(
type=self.identifier,
identifier=identifier,
organizer=organizer,
active=True,
linked_giftcard=gc
)
m.log_action(
'pretix.reusable_medium.created.auto',
user=user, auth=auth,
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return m
MEDIA_TYPES = {
m.identifier: m for m in [
BarcodePlainMediaType(),
NfcUidMediaType(),
]
}

View File

@@ -0,0 +1,77 @@
# Generated by Django 3.2.18 on 2023-02-20 12:46
import django.core.serializers.json
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
def set_can_manage_reusable_media(apps, schema_editor):
Team = apps.get_model('pretixbase', 'Team')
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_reusable_media=True)
Team.objects.filter(can_change_orders=True, all_events=True).update(can_manage_reusable_media=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0235_auto_20230316_2023'),
]
operations = [
migrations.AddField(
model_name='item',
name='media_policy',
field=models.CharField(max_length=16, null=True),
),
migrations.AddField(
model_name='item',
name='media_type',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='team',
name='can_manage_reusable_media',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='ReusableMedium',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('type', models.CharField(max_length=100)),
('identifier', models.CharField(max_length=200)),
('active', models.BooleanField(default=True)),
('expires', models.DateTimeField(blank=True, null=True)),
('info', models.JSONField(default=dict)),
('notes', models.TextField(null=True, blank=True)),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='reusable_media', to='pretixbase.customer')),
('linked_giftcard',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
to='pretixbase.giftcard')),
('linked_orderposition',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
to='pretixbase.orderposition')),
('organizer',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reusable_media',
to='pretixbase.organizer')),
],
options={
'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')},
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AddField(
model_name='checkin',
name='raw_source_type',
field=models.CharField(max_length=100, null=True),
),
migrations.RunPython(
set_can_manage_reusable_media,
migrations.RunPython.noop,
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-04-05 10:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0236_reusable_media'),
]
operations = [
migrations.AddField(
model_name='question',
name='valid_string_length_max',
field=models.PositiveIntegerField(null=True),
),
]

View File

@@ -40,6 +40,7 @@ from .items import (
SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .media import ReusableMedium
from .memberships import Membership, MembershipType
from .notifications import NotificationSetting
from .orders import (

View File

@@ -44,6 +44,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.helpers import PostgresWindowFrame
@@ -377,6 +378,11 @@ class Checkin(models.Model):
# 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_source_type = models.CharField(
max_length=100,
null=True, blank=True,
choices=[(k, v) for k, v in MEDIA_TYPES.items()],
)
raw_item = models.ForeignKey(
'pretixbase.Item',
related_name='checkins',

View File

@@ -142,6 +142,7 @@ class Customer(LoggedModel):
self.save()
self.all_logentries().update(data={}, shredded=True)
self.orders.all().update(customer=None)
self.reusable_media.all().update(customer=None)
self.memberships.all().update(attendee_name_parts=None)
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
@@ -221,7 +222,7 @@ class Customer(LoggedModel):
return salted_hmac(key_salt, payload).hexdigest()
def get_email_context(self):
from pretix.base.email import get_name_parts_localized
from pretix.base.settings import get_name_parts_localized
ctx = {
'name': self.name,
'organizer': self.organizer.name,

View File

@@ -180,7 +180,8 @@ class Device(LoggedModel):
'can_view_orders',
'can_change_orders',
'can_view_vouchers',
'can_manage_gift_cards'
'can_manage_gift_cards',
'can_manage_reusable_media',
}
def get_event_permission_set(self, organizer, event) -> set:

View File

@@ -45,7 +45,9 @@ import dateutil.parser
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, RegexValidator
from django.core.validators import (
MaxLengthValidator, MinValueValidator, RegexValidator,
)
from django.db import models
from django.db.models import Q
from django.utils import formats
@@ -64,6 +66,7 @@ from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
from .event import Event, SubEvent
@@ -368,6 +371,16 @@ class Item(LoggedModel):
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
)
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
MEDIA_POLICIES = (
(None, _("Don't use re-usable media, use regular one-off tickets")),
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')),
)
objects = ItemQuerySetManager()
event = models.ForeignKey(
@@ -630,6 +643,29 @@ class Item(LoggedModel):
help_text=_('The selected start date may only be this many days in the future.')
)
media_policy = models.CharField(
choices=MEDIA_POLICIES,
null=True, blank=True, max_length=16,
verbose_name=_('Reusable media policy'),
help_text=_(
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. '
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
'renewable season tickets or re-chargeable gift card wristbands. '
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
)
)
media_type = models.CharField(
max_length=100,
null=True, blank=True,
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
verbose_name=_('Reusable media type'),
help_text=_(
'Select the type of physical medium that should be used for this product. Note that not all media types '
'support all types of products, and not all media types are supported across all sales channels or '
'check-in processes.'
)
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
@@ -801,6 +837,24 @@ class Item(LoggedModel):
def has_variations(self):
return self.variations.exists()
@staticmethod
def clean_media_settings(event, media_policy, media_type, issue_giftcard):
if media_policy:
if not media_type:
raise ValidationError(_('If you select a reusable media policy, you also need to select a reusable '
'media type.'))
mt = MEDIA_TYPES[media_type]
if not mt.is_active(event.organizer):
raise ValidationError(_('The selected media type is not enabled in your organizer settings.'))
if not mt.supports_orderposition and not issue_giftcard:
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
if not mt.supports_giftcard and issue_giftcard:
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
if issue_giftcard:
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
'gift cards for some reusable media types can be created or re-charged directly '
'at the POS.'))
@staticmethod
def clean_per_order(min_per_order, max_per_order):
if min_per_order is not None and max_per_order is not None:
@@ -1488,6 +1542,11 @@ class Question(LoggedModel):
valid_datetime_max = models.DateTimeField(null=True, blank=True,
verbose_name=_('Maximum value'),
help_text=_('Currently not supported in our apps and during check-in'))
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
verbose_name=_('Maximum length'),
help_text=_(
'Currently not supported in our apps and during check-in'
))
valid_file_portrait = models.BooleanField(
default=False,
verbose_name=_('Validate file to be a portrait'),
@@ -1634,6 +1693,12 @@ class Question(LoggedModel):
return answer
else:
raise ValidationError(_('Unknown country code.'))
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
raise ValidationError(MaxLengthValidator.message % {
'limit_value': self.valid_string_length_max,
'show_value': len(answer)
})
return answer

View File

@@ -0,0 +1,125 @@
#
# 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 django.db import models
from django.db.models import Q
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import LoggedModel
from pretix.base.models.customers import Customer
from pretix.base.models.giftcards import GiftCard
from pretix.base.models.orders import OrderPosition
from pretix.base.models.organizer import Organizer
class ReusableMediumQuerySet(models.QuerySet):
def active(self):
return self.filter(
Q(expires__isnull=True) | Q(expires__gte=now()),
active=True,
)
class ReusableMediumQuerySetManager(ScopedManager(organizer='organizer').__class__):
def __init__(self):
super().__init__()
self._queryset_class = ReusableMediumQuerySet
def active(self):
return self.get_queryset().active()
class ReusableMedium(LoggedModel):
id = models.BigAutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
organizer = models.ForeignKey(
Organizer,
related_name='reusable_media',
on_delete=models.PROTECT
)
type = models.CharField(
verbose_name=pgettext_lazy('reusable_medium', 'Media type'),
choices=((k, v) for k, v in MEDIA_TYPES.items()),
max_length=100,
)
identifier = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
)
active = models.BooleanField(
verbose_name=_('Active'),
default=True
)
expires = models.DateTimeField(
verbose_name=_('Expiration date'),
null=True, blank=True
)
customer = models.ForeignKey(
Customer,
null=True, blank=True,
related_name='reusable_media',
on_delete=models.SET_NULL,
verbose_name=_('Customer account'),
)
linked_orderposition = models.ForeignKey(
OrderPosition,
null=True, blank=True,
related_name='linked_media',
on_delete=models.SET_NULL,
verbose_name=_('Linked ticket'),
)
linked_giftcard = models.ForeignKey(
GiftCard,
null=True, blank=True,
related_name='linked_media',
on_delete=models.SET_NULL,
verbose_name=_('Linked gift card'),
)
info = models.JSONField(
default=dict
)
notes = models.TextField(verbose_name=_('Notes'), null=True, blank=True)
objects = ReusableMediumQuerySetManager()
@cached_property
def media_type(self):
return MEDIA_TYPES[self.type]
@property
def is_expired(self):
return self.expires and self.expires > now()
class Meta:
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
ordering = "identifier", "type", "organizer"

View File

@@ -236,6 +236,8 @@ class Team(LoggedModel):
:type can_change_teams: bool
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
:type can_manage_customers: bool
:param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media.
:type can_manage_reusable_media: bool
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
:type can_change_organizer_settings: bool
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
@@ -277,6 +279,10 @@ class Team(LoggedModel):
default=False,
verbose_name=_("Can manage customer accounts")
)
can_manage_reusable_media = models.BooleanField(
default=False,
verbose_name=_("Can manage reusable media")
)
can_manage_gift_cards = models.BooleanField(
default=False,
verbose_name=_("Can manage gift cards")

View File

@@ -454,7 +454,7 @@ class Voucher(LoggedModel):
@staticmethod
def clean_voucher_code(data, event, pk):
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code'].upper()) & Q(event=event) & ~Q(pk=pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
@staticmethod

View File

@@ -219,18 +219,19 @@ class WaitingListEntry(LoggedModel):
self.voucher = v
self.save()
self.send_mail(
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(
event=self.event,
waiting_list_entry=self,
waiting_list_voucher=v,
event_or_subevent=self.subevent or self.event,
),
user=user,
auth=auth,
)
with language(self.locale, self.event.settings.region):
self.send_mail(
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(
event=self.event,
waiting_list_entry=self,
waiting_list_voucher=v,
event_or_subevent=self.subevent or self.event,
),
user=user,
auth=auth,
)
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.waitinglist.email.sent',

View File

@@ -360,7 +360,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
'{} - {}'.format(p.item.name, p.variation.value) if p.variation else str(p.item.name)
for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
@@ -411,7 +411,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
now().astimezone(timezone(ev.settings.timezone)),
op.valid_from.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT"
) if op.valid_from else ""
}),
@@ -455,6 +455,11 @@ DEFAULT_VARIABLES = OrderedDict((
"TIME_FORMAT"
) if op.valid_until else ""
}),
("medium_identifier", {
"label": _("Reusable Medium ID"),
"editor_sample": "ABC1234DEF4567",
"evaluate": lambda op, order, ev: op.linked_media.all()[0].identifier if op.linked_media.all() else "",
}),
("seat", {
"label": _("Seat: Full name"),
"editor_sample": _("Ground floor, Row 3, Seat 4"),
@@ -583,10 +588,11 @@ def variables_from_questions(sender, *args, **kwargs):
def _get_attendee_name_part(key, op, order, ev):
name_parts = op.attendee_name_parts or (op.addon_to.attendee_name_parts if op.addon_to else {})
if isinstance(key, tuple):
parts = [_get_attendee_name_part(c[0], op, order, ev) for c in key if not (c[0] == 'salutation' and op.attendee_name_parts.get(c[0], '') == "Mx")]
parts = [_get_attendee_name_part(c[0], op, order, ev) for c in key if not (c[0] == 'salutation' and name_parts.get(c[0], '') == "Mx")]
return ' '.join(p for p in parts if p)
value = op.attendee_name_parts.get(key, '')
value = name_parts.get(key, '')
if key == 'salutation':
return pgettext('person_name_salutation', value)
return value
@@ -617,7 +623,7 @@ def get_variables(event):
v['attendee_name_for_salutation'] = {
'label': _("Attendee name for salutation"),
'editor_sample': _("Mr Doe"),
'evaluate': lambda op, order, ev: concatenation_for_salutation(op.attendee_name_parts or {})
'evaluate': lambda op, order, ev: concatenation_for_salutation(op.attendee_name_parts or (op.addon_to.attendee_name_parts if op.addon_to else {}))
}
for key, label, weight in scheme['fields']:
@@ -761,6 +767,9 @@ class Renderer:
else:
content = self._get_text_content(op, order, o)
if len(content) == 0:
return
level = 'H'
if len(content) > 32:
level = 'M'

View File

@@ -24,9 +24,11 @@ import sys
from enum import Enum
from typing import List
import importlib_metadata as metadata
from django.apps import AppConfig, apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from packaging.requirements import Requirement
class PluginType(Enum):
@@ -81,12 +83,11 @@ class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
raise ImproperlyConfigured("A pretix plugin config should have a PretixPluginMeta inner class.")
if hasattr(self.PretixPluginMeta, 'compatibility') and not os.environ.get("PRETIX_IGNORE_CONFLICTS") == "True":
import pkg_resources
try:
pkg_resources.require(self.PretixPluginMeta.compatibility)
except pkg_resources.VersionConflict as e:
req = Requirement(self.PretixPluginMeta.compatibility)
requirement_version = metadata.version(req.name)
if not req.specifier.contains(requirement_version, prereleases=True):
print("Incompatible plugins found!")
print("Plugin {} requires you to have {}, but you installed {}.".format(
self.name, e.req, e.dist
self.name, req, requirement_version
))
sys.exit(1)

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.
import re
import uuid
from collections import Counter, defaultdict, namedtuple
from datetime import datetime, time, timedelta
@@ -38,6 +39,7 @@ from decimal import Decimal
from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django import forms
from django.core.exceptions import ValidationError
from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
@@ -50,6 +52,7 @@ from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
SeatCategoryMapping, Voucher,
@@ -135,6 +138,7 @@ error_messages = {
'some_subevent_ended': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_not_a_number': gettext_lazy('The entered price is not a number.'),
'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
@@ -196,6 +200,8 @@ error_messages = {
'seat_multiple': gettext_lazy('You can not select the same seat multiple times.'),
'gift_card': gettext_lazy("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
'media_usage_not_implemented': gettext_lazy('The configuration of this product requires mapping to a physical '
'medium, which is currently not available online.'),
}
@@ -391,6 +397,13 @@ class CartManager:
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
raise CartError(error_messages['unavailable'])
if op.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
mt = MEDIA_TYPES[op.item.media_type]
if not mt.medium_created_by_server:
raise CartError(error_messages['media_usage_not_implemented'])
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
raise CartError(error_messages['media_usage_not_implemented'])
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'])
@@ -725,9 +738,18 @@ class CartManager:
price_after_voucher = listed_price
custom_price = None
if item.free_price and i.get('price'):
custom_price = Decimal(str(i.get('price')).replace(",", "."))
custom_price = re.sub('[^0-9.,]', '', str(i.get('price')))
if not custom_price:
raise CartError(error_messages['price_not_a_number'])
try:
custom_price = forms.DecimalField(localize=True).to_python(custom_price)
except:
try:
custom_price = Decimal(custom_price)
except:
raise CartError(error_messages['price_not_a_number'])
if custom_price > 99_999_999_999:
raise ValueError('price_too_high')
raise CartError(error_messages['price_too_high'])
op = self.AddOperation(
count=i['count'],
@@ -840,9 +862,18 @@ class CartManager:
listed_price = get_listed_price(item, variation, cp.subevent)
custom_price = None
if item.free_price and a.get('price'):
custom_price = Decimal(str(a.get('price')).replace(",", "."))
custom_price = re.sub('[^0-9.,]', '', a.get('price'))
if not custom_price:
raise CartError(error_messages['price_not_a_number'])
try:
custom_price = forms.DecimalField(localize=True).to_python(custom_price)
except:
try:
custom_price = Decimal(custom_price)
except:
raise CartError(error_messages['price_not_a_number'])
if custom_price > 99_999_999_999:
raise ValueError('price_too_high')
raise CartError(error_messages['price_too_high'])
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
for ca in current_addons[cp][a['item'], a['variation']]:

View File

@@ -693,7 +693,7 @@ 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,
raw_barcode=None, from_revoked_secret=False):
raw_barcode=None, raw_source_type=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.
@@ -714,40 +714,53 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
# !!!!!!!!!
dt = datetime or now()
force_used = False
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
raise CheckInError(
_('This order position has been canceled.'),
'canceled' if canceled_supported else 'unpaid'
)
if force:
force_used = True
else:
raise CheckInError(
_('This order position has been canceled.'),
'canceled' if canceled_supported else 'unpaid'
)
if op.blocked:
raise CheckInError(
_('This ticket has been blocked.'), # todo provide reason
'blocked'
)
if force:
force_used = True
else:
raise CheckInError(
_('This ticket has been blocked.'), # todo provide reason
'blocked'
)
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
raise CheckInError(
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
),
)
if force:
force_used = True
else:
raise CheckInError(
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket is only valid after {datetime}.').format(
datetime=date_format(op.valid_from.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
),
)
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
raise CheckInError(
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
),
)
if force:
force_used = True
else:
raise CheckInError(
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
),
'invalid_time',
_('This ticket was only valid before {datetime}.').format(
datetime=date_format(op.valid_until.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
),
)
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
checkin_questions = list(
@@ -770,40 +783,57 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
op = opqs.get(pk=op.pk)
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
raise CheckInError(
_('This order position has an invalid product for this check-in list.'),
'product'
)
elif clist.subevent_id and op.subevent_id != clist.subevent_id:
raise CheckInError(
_('This order position has an invalid date for this check-in list.'),
'product'
)
elif op.order.status != Order.STATUS_PAID and not force and op.order.require_approval:
raise CheckInError(
_('This order is not yet approved.'),
'unpaid'
)
elif op.order.status != Order.STATUS_PAID and not force and not op.order.valid_if_pending and not (
if force:
force_used = True
else:
raise CheckInError(
_('This order position has an invalid product for this check-in list.'),
'product'
)
if clist.subevent_id and op.subevent_id != clist.subevent_id:
if force:
force_used = True
else:
raise CheckInError(
_('This order position has an invalid date for this check-in list.'),
'product'
)
if op.order.status != Order.STATUS_PAID and op.order.require_approval:
if force:
force_used = True
else:
raise CheckInError(
_('This order is not yet approved.'),
'unpaid'
)
elif op.order.status != Order.STATUS_PAID and not op.order.valid_if_pending and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
raise CheckInError(
_('This order is not marked as paid.'),
'unpaid'
)
if force:
force_used = True
else:
raise CheckInError(
_('This order is not marked as paid.'),
'unpaid'
)
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
if type == Checkin.TYPE_ENTRY and clist.rules:
rule_data = LazyRuleVars(op, clist, dt)
logic = _get_logic_environment(op.subevent or clist.event)
if not logic.apply(clist.rules, rule_data):
reason = _logic_explain(clist.rules, op.subevent or clist.event, rule_data)
raise CheckInError(
_('Entry not permitted: {explanation}.').format(
explanation=reason
),
'rules',
reason=reason
)
if force:
force_used = True
else:
reason = _logic_explain(clist.rules, op.subevent or clist.event, rule_data)
raise CheckInError(
_('Entry not permitted: {explanation}.').format(
explanation=reason
),
'rules',
reason=reason
)
if require_answers and not force and questions_supported:
raise RequiredQuestionsError(
@@ -837,9 +867,10 @@ 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 or from_revoked_secret),
forced=force and (not entry_allowed or from_revoked_secret or force_used),
force_sent=force,
raw_barcode=raw_barcode,
raw_source_type=raw_source_type,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,

View File

@@ -95,6 +95,18 @@ class SendMailException(Exception):
pass
def clean_sender_name(sender_name: str) -> str:
# Emails with @ in their sender name are rejected by some mailservers (e.g. Microsoft) because it looks like
# a phishing attempt.
sender_name = sender_name.replace("@", " ")
# Emails with excessively long sender names are rejected by some mailservers
if len(sender_name) > 75:
sender_name = sender_name[:75] + "..."
return sender_name
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
@@ -196,17 +208,13 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
settings.MAIL_FROM
)
if event:
sender_name = event.settings.mail_from_name or str(event.name)
if len(sender_name) > 75:
sender_name = sender_name[:75] + "..."
sender_name = clean_sender_name(event.settings.mail_from_name or str(event.name))
sender = formataddr((sender_name, sender))
elif organizer:
sender_name = organizer.settings.mail_from_name or str(organizer.name)
if len(sender_name) > 75:
sender_name = sender_name[:75] + "..."
sender_name = clean_sender_name(organizer.settings.mail_from_name or str(organizer.name))
sender = formataddr((sender_name, sender))
else:
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
sender = formataddr((clean_sender_name(settings.PRETIX_INSTANCE_NAME), sender))
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
signature = ""

View File

@@ -62,6 +62,7 @@ from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -390,9 +391,15 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
if order.total == Decimal('0.00'):
email_template = order.event.settings.mail_text_order_approved_free
email_subject = order.event.settings.mail_subject_order_approved_free
email_attendees = order.event.settings.mail_send_order_approved_free_attendee
email_attendee_template = order.event.settings.mail_text_order_approved_free_attendee
email_attendee_subject = order.event.settings.mail_subject_order_approved_free_attendee
else:
email_template = order.event.settings.mail_text_order_approved
email_subject = order.event.settings.mail_subject_order_approved
email_attendees = order.event.settings.mail_send_order_approved_attendee
email_attendee_template = order.event.settings.mail_text_order_approved_attendee
email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee
email_context = get_email_context(event=order.event, order=order)
try:
@@ -405,6 +412,19 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
except SendMailException:
logger.exception('Order approved email could not be sent')
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
email_attendee_context = get_email_context(event=order.event, order=order, position=p)
try:
p.send_mail(
email_attendee_subject, email_attendee_template, email_attendee_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
)
except SendMailException:
logger.exception('Order approved email could not be sent to attendee')
return order.pk
@@ -1431,7 +1451,7 @@ class OrderChangeManager:
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
'valid_from', 'valid_until'))
'valid_from', 'valid_until', 'is_bundled'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
@@ -1661,6 +1681,7 @@ class OrderChangeManager:
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
is_bundled = False
if price is None:
raise OrderError(self.error_messages['product_invalid'])
if item.variations.exists() and not variation:
@@ -1669,7 +1690,10 @@ class OrderChangeManager:
raise OrderError(self.error_messages['addon_to_required'])
if addon_to:
if not item.category or item.category_id not in addon_to.item.addons.values_list('addon_category', flat=True):
raise OrderError(self.error_messages['addon_invalid'])
if addon_to.item.bundles.filter(bundled_item=item, bundled_variation=variation).exists():
is_bundled = True
else:
raise OrderError(self.error_messages['addon_invalid'])
if self.order.event.has_subevents and not subevent:
raise OrderError(self.error_messages['subevent_required'])
@@ -1694,7 +1718,7 @@ class OrderChangeManager:
if seat:
self._seatdiff.update([seat])
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
valid_from, valid_until))
valid_from, valid_until, is_bundled))
def split(self, position: OrderPosition):
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
@@ -2225,6 +2249,7 @@ class OrderChangeManager:
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
is_bundled=op.is_bundled,
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
@@ -2911,3 +2936,32 @@ def signal_listener_issue_memberships(sender: Event, order: Order, **kwargs):
for p in order.positions.all():
if p.item.grant_membership_type_id:
create_membership(order.customer, p)
@receiver(order_placed, dispatch_uid="pretixbase_order_placed_media")
@receiver(order_changed, dispatch_uid="pretixbase_order_changed_media")
@transaction.atomic()
def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
from pretix.base.models import ReusableMedium
for p in order.positions.all():
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
mt = MEDIA_TYPES[p.item.media_type]
if mt.medium_created_by_server and not p.linked_media.exists():
rm = ReusableMedium.objects.create(
organizer=sender.organizer,
type=p.item.media_type,
identifier=mt.generate_identifier(sender.organizer),
active=True,
customer=order.customer,
linked_orderposition=p,
)
rm.log_action(
'pretix.reusable_medium.created',
data={
'by_order': order.code,
'linked_orderposition': p.pk,
'active': True,
'customer': order.customer_id,
}
)

View File

@@ -19,9 +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 re
from decimal import Decimal
from typing import List, Optional, Tuple
from django import forms
from django.db.models import Q
from django.utils.timezone import now
@@ -69,7 +71,16 @@ def get_price(item: Item, variation: ItemVariation = None,
subtract_from_gross=bundled_sum)
elif item.free_price and custom_price is not None and custom_price != "":
if not isinstance(custom_price, Decimal):
custom_price = Decimal(str(custom_price).replace(",", "."))
custom_price = re.sub('[^0-9.,]', '', str(custom_price))
if not custom_price:
raise ValueError('price_not_a_number')
try:
custom_price = forms.DecimalField(localize=True).to_python(custom_price)
except:
try:
custom_price = Decimal(custom_price)
except:
raise ValueError('price_not_a_number')
if custom_price > 99_999_999_999:
raise ValueError('price_too_high')

View File

@@ -169,6 +169,84 @@ DEFAULTS = {
"was not logged in during the purchase.")
)
},
'reusable_media_active': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Activate re-usable media"),
help_text=_("The re-usable media feature allows you to connect tickets and gift cards with physical media "
"such as wristbands or chip cards that may be re-used for different tickets or gift cards "
"later.")
)
},
'reusable_media_type_barcode': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Active"),
)
},
'reusable_media_type_barcode_identifier_length': {
'default': 24,
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(
validators=[
MinValueValidator(12),
MaxValueValidator(64),
]
),
'form_kwargs': dict(
label=_('Length of barcodes'),
validators=[
MinValueValidator(12),
MaxValueValidator(64),
],
required=True,
widget=forms.NumberInput(
attrs={
'min': '12',
'max': '64',
},
),
)
},
'reusable_media_type_nfc_uid': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Active"),
)
},
'reusable_media_type_nfc_uid_autocreate_giftcard': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Automatically create a new gift card if a previously unknown chip is seen"),
)
},
'reusable_media_type_nfc_uid_autocreate_giftcard_currency': {
'default': 'EUR',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
),
'form_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
label=_("Gift card currency"),
)
},
'max_items_per_order': {
'default': '10',
'type': int,
@@ -212,6 +290,8 @@ DEFAULTS = {
'system_question_order': {
'default': {},
'type': dict,
'serializer_class': serializers.DictField,
'serializer_kwargs': lambda: dict(read_only=True, allow_empty=True),
},
'attendee_names_asked': {
'default': 'True',
@@ -514,6 +594,7 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Minimum length of invoice number after prefix"),
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
max_value=12,
required=True,
)
},
@@ -1291,9 +1372,10 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Generate tickets for add-on products"),
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
'products. With this option, a separate ticket is issued for every add-on product as well.'),
label=_("Generate tickets for add-on products and bundled products"),
help_text=_('By default, tickets are only issued for products selected individually, not for add-on products '
'or bundled products. With this option, a separate ticket is issued for every add-on product '
'or bundled product as well.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
)
@@ -2193,6 +2275,26 @@ You can select a payment method and perform the payment here:
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_approved_attendee': {
'type': bool,
'default': 'False'
},
'mail_subject_order_approved_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
},
'mail_text_order_approved_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we approved a ticket ordered for you for {event}.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -2210,6 +2312,26 @@ at our event. As you only ordered free products, no payment is required.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_approved_free_attendee': {
'type': bool,
'default': 'False'
},
'mail_subject_order_approved_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
},
'mail_text_order_approved_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we approved a ticket ordered for you for {event}.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -3051,6 +3173,13 @@ def concatenation_for_salutation(d):
return " ".join(filter(None, (salutation, title, given_name, family_name)))
def get_name_parts_localized(name_parts, key):
value = name_parts.get(key, "")
if key == "salutation":
return pgettext_lazy("person_name_salutation", value)
return value
PERSON_NAME_SCHEMES = OrderedDict([
('given_family', {
'fields': (
@@ -3407,7 +3536,12 @@ def validate_organizer_settings(organizer, settings_dict):
# organizer-settings either.
#
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
pass
"""
if settings_dict.get('reusable_media_type_ntag_pretix1') and settings_dict.get('reusable_media_type_nfc_uid'):
raise ValidationError({
'reusable_media_type_nfc_uid': _('This needs to be disabled if other NFC-based types are active.')
})
"""
def global_settings_object(holder):

View File

@@ -71,6 +71,7 @@ class BaseQuestionsViewMixin:
kwargs = self.question_form_kwargs(cr)
form = self.form_class(event=self.request.event,
prefix=cr.id,
request=self.request,
cartpos=cartpos,
orderpos=orderpos,
all_optional=self.all_optional,

View File

@@ -39,7 +39,7 @@ from urllib.parse import urlencode, urlparse
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.core.validators import MaxValueValidator
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import (
@@ -457,7 +457,49 @@ class EventUpdateForm(I18nModelForm):
}
class EventSettingsForm(SettingsForm):
class EventSettingsValidationMixin:
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_event_settings(self.obj, settings_dict)
return data
def add_error(self, field, error):
# Copied from Django, but with improved handling for validation errors on fields that are not part of this form
if not isinstance(error, ValidationError):
error = ValidationError(error)
if hasattr(error, 'error_dict'):
if field is not None:
raise TypeError(
"The argument `field` must be `None` when the `error` "
"argument contains errors for multiple fields."
)
else:
error = error.error_dict
else:
error = {field or NON_FIELD_ERRORS: error.error_list}
for field, error_list in error.items():
if field != NON_FIELD_ERRORS and field not in self.fields:
field = NON_FIELD_ERRORS
for e in error_list:
e.message = _('A validation error has occurred on a setting that is not part of this form: {error}').format(error=e.message)
if field not in self.errors:
if field == NON_FIELD_ERRORS:
self._errors[field] = self.error_class(error_class='nonfield')
else:
self._errors[field] = self.error_class()
self._errors[field].extend(error_list)
if field in self.cleaned_data:
del self.cleaned_data[field]
class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Event timezone"),
@@ -573,13 +615,8 @@ class EventSettingsForm(SettingsForm):
return data
def clean(self):
self.cleaned_data = self._resolve_virtual_keys_input(self.cleaned_data)
data = super().clean()
settings_dict = self.event.settings.freeze()
settings_dict.update(data)
data = self._resolve_virtual_keys_input(data)
validate_event_settings(self.event, data)
return data
def __init__(self, *args, **kwargs):
@@ -702,7 +739,7 @@ class CancelSettingsForm(SettingsForm):
).format(self.obj.settings.giftcard_expiry_years)
class PaymentSettingsForm(SettingsForm):
class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
auto_fields = [
'payment_term_mode',
'payment_term_days',
@@ -734,13 +771,6 @@ class PaymentSettingsForm(SettingsForm):
raise ValidationError(_("This field is required."))
return value
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_event_settings(self.obj, data)
return data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tax_rate_default'].queryset = self.obj.tax_rules.all()
@@ -788,7 +818,7 @@ class ProviderForm(SettingsForm):
return cleaned_data
class InvoiceSettingsForm(SettingsForm):
class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
auto_fields = [
'invoice_address_asked',
@@ -863,13 +893,6 @@ class InvoiceSettingsForm(SettingsForm):
)
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_event_settings(self.obj, data)
return data
def contains_web_channel_validate(val):
if "web" not in val:
@@ -1168,6 +1191,24 @@ class MailSettingsForm(SettingsForm):
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
mail_send_order_approved_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_order_approved_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_order_approved_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
mail_subject_order_approved_free = I18nFormField(
label=_("Subject for approved free order"),
required=False,
@@ -1180,6 +1221,24 @@ class MailSettingsForm(SettingsForm):
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
mail_send_order_approved_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_order_approved_free_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_order_approved_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
mail_subject_order_denied = I18nFormField(
label=_("Subject for denied order"),
required=False,

View File

@@ -256,9 +256,13 @@ class OrderFilterForm(FilterForm):
else:
code = Q(code__icontains=Order.normalize_code(u))
invoice_nos = {u, u.upper()}
if u.isdigit():
for i in range(2, 12):
invoice_nos.add(u.zfill(i))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
Q(invoice_no__in=invoice_nos)
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.objects.filter(
@@ -569,6 +573,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
label=_('Sales channel'),
required=False,
)
checkin_attention = forms.NullBooleanField(
required=False,
widget=FilterNullBooleanSelect,
label=_('Requires special attention'),
help_text=_('Only matches orders with the attention checkbox set directly for the order, not based on the product.'),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -689,6 +699,8 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
qs = qs.filter(total=fdata.get('total'))
if fdata.get('email_known_to_work') is not None:
qs = qs.filter(email_known_to_work=fdata.get('email_known_to_work'))
if fdata.get('checkin_attention') is not None:
qs = qs.filter(checkin_attention=fdata.get('checkin_attention'))
if fdata.get('locale'):
qs = qs.filter(locale=fdata.get('locale'))
if fdata.get('payment_sum_min') is not None:
@@ -996,9 +1008,13 @@ class OrderPaymentSearchFilterForm(forms.Form):
if fdata.get('query'):
u = fdata.get('query')
invoice_nos = {u, u.upper()}
if u.isdigit():
for i in range(2, 12):
invoice_nos.add(u.zfill(i))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
Q(invoice_no__in=invoice_nos)
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
@@ -1419,6 +1435,62 @@ class CustomerFilterForm(FilterForm):
return qs.distinct()
class ReusableMediaFilterForm(FilterForm):
orders = {
'type': 'type',
'identifier': 'identifier',
}
query = forms.CharField(
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
required=False,
choices=(
('', _('All')),
('active', _('active')),
('disabled', _('disabled')),
('expired', _('expired')),
)
)
def __init__(self, *args, **kwargs):
kwargs.pop('request')
super().__init__(*args, **kwargs)
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(identifier__icontains=query)
| Q(customer__identifier__icontains=query)
| Q(customer__external_identifier__istartswith=query)
| Q(linked_orderposition__order__code__icontains=query)
| Q(linked_giftcard__secret__icontains=query)
)
if fdata.get('status') == 'active':
qs = qs.filter(Q(expires__gt=now()) | Q(expires__isnull=False), active=True)
elif fdata.get('status') == 'disabled':
qs = qs.filter(active=False)
elif fdata.get('status') == 'expired':
qs = qs.filter(expires__lte=now())
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by("identifier", "type", "organizer")
return qs.distinct()
class TeamFilterForm(FilterForm):
orders = {
'name': 'name',

View File

@@ -168,6 +168,7 @@ class QuestionForm(I18nModelForm):
'valid_date_min',
'valid_date_max',
'valid_file_portrait',
'valid_string_length_max',
]
widgets = {
'valid_datetime_min': SplitDateTimePickerWidget(),
@@ -401,6 +402,8 @@ class ItemCreateForm(I18nModelForm):
'validity_dynamic_duration_months',
'validity_dynamic_start_choice',
'validity_dynamic_start_choice_day_limit',
'media_type',
'media_policy',
)
for f in fields:
setattr(self.instance, f, getattr(src, f))
@@ -592,6 +595,10 @@ class ItemUpdateForm(I18nModelForm):
del self.fields['grant_membership_duration_days']
del self.fields['grant_membership_duration_months']
if not self.event.settings.reusable_media_active:
del self.fields['media_type']
del self.fields['media_policy']
def clean(self):
d = super().clean()
if d['issue_giftcard']:
@@ -635,6 +642,8 @@ class ItemUpdateForm(I18nModelForm):
_("The start of validity must be before the end of validity.")
)
Item.clean_media_settings(self.event, d.get('media_policy'), d.get('media_type'), d.get('issue_giftcard'))
return d
def clean_picture(self):
@@ -693,6 +702,8 @@ class ItemUpdateForm(I18nModelForm):
'validity_dynamic_duration_months',
'validity_dynamic_start_choice',
'validity_dynamic_start_choice_day_limit',
'media_policy',
'media_type',
]
field_classes = {
'available_from': SplitDateTimeField,

View File

@@ -41,10 +41,14 @@ from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import inlineformset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from i18nfield.forms import (
I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
@@ -62,15 +66,18 @@ from pretix.base.forms.questions import (
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
MembershipType, Organizer, Team,
MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_organizer_settings,
)
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import (
SafeEventMultipleChoiceField, multimail_validate,
)
from pretix.control.forms.widgets import Select2
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -208,6 +215,7 @@ class TeamForm(forms.ModelForm):
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
'can_change_teams', 'can_change_organizer_settings',
'can_manage_gift_cards', 'can_manage_customers',
'can_manage_reusable_media',
'can_change_event_settings', 'can_change_items',
'can_view_orders', 'can_change_orders', 'can_checkin_orders',
'can_view_vouchers', 'can_change_vouchers']
@@ -389,6 +397,12 @@ class OrganizerSettingsForm(SettingsForm):
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
organizer_logo_image = ExtFileField(
@@ -431,6 +445,26 @@ class OrganizerSettingsForm(SettingsForm):
))
for k, v in PERSON_NAME_TITLE_GROUPS.items()
]
self.fields['reusable_media_active'].label = mark_safe(
conditional_escape(self.fields['reusable_media_active'].label) +
' ' +
'<span class="label label-info">{}</span>'.format(_('experimental'))
)
self.fields['reusable_media_active'].help_text = mark_safe(
conditional_escape(self.fields['reusable_media_active'].help_text) +
' ' +
'<br/><span class="fa fa-flask"></span> ' +
_('This feature is currently in an experimental stage. It only supports very limited use cases and might '
'change at any point.')
)
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_organizer_settings(self.obj, data)
return data
class MailSettingsForm(SettingsForm):
@@ -626,6 +660,116 @@ class GiftCardUpdateForm(forms.ModelForm):
}
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
def label_from_instance(self, op):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class ReusableMediumUpdateForm(forms.ModelForm):
error_messages = {
'duplicate': _("An medium with this type and identifier is already registered."),
}
class Meta:
model = ReusableMedium
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
organizer = self.instance.organizer
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderposition'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Ticket')
}
)
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
self.fields['linked_orderposition'].required = False
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
self.fields['linked_giftcard'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.giftcards.select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Gift card')
}
)
self.fields['linked_giftcard'].widget.choices = self.fields['linked_giftcard'].choices
self.fields['linked_giftcard'].required = False
if organizer.settings.customer_accounts:
self.fields['customer'].queryset = organizer.customers.all()
self.fields['customer'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Customer')
}
)
self.fields['customer'].widget.choices = self.fields['customer'].choices
self.fields['customer'].required = False
else:
del self.fields['customer']
def clean(self):
identifier = self.cleaned_data.get('identifier')
type = self.cleaned_data.get('type')
if identifier is not None and type is not None:
try:
self.instance.organizer.reusable_media.exclude(pk=self.instance.pk).get(
identifier=identifier,
type=type,
)
except ReusableMedium.DoesNotExist:
pass
else:
raise forms.ValidationError(
self.error_messages['duplicate'],
code='duplicate',
)
return self.cleaned_data
class ReusableMediumCreateForm(ReusableMediumUpdateForm):
class Meta:
model = ReusableMedium
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
}
class CustomerUpdateForm(forms.ModelForm):
error_messages = {
'duplicate': _("An account with this email address is already registered."),

View File

@@ -350,6 +350,8 @@ class VoucherBulkForm(VoucherForm):
reader = csv.DictReader(StringIO(raw), dialect=dialect)
except csv.Error as e:
raise ValidationError(_('CSV parsing failed: {error}.').format(error=str(e)))
if len(reader.fieldnames) == 1 and ',' in reader.fieldnames[0]:
raise ValidationError(_('CSV input was not recognized to have multiple columns, maybe you have some invalid quoted field in your input.'))
if 'email' not in reader.fieldnames:
raise ValidationError(_('CSV input needs to contain a field with the header "{header}".').format(header="email"))
unknown_fields = [f for f in reader.fieldnames if f not in ('email', 'name', 'tag', 'number')]

View File

@@ -362,6 +362,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.customer.anonymized': _('The account has been disabled and anonymized.'),
'pretix.customer.password.resetrequested': _('A new password has been requested.'),
'pretix.customer.password.set': _('A new password has been set.'),
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.email.error': _('Sending of an email has failed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'),
@@ -470,6 +474,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
'pretix.event.item.reordered': _('The product has been reordered.'),
'pretix.event.item.deleted': _('The product has been deleted.'),
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
@@ -488,9 +493,11 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.category.added': _('The category has been added.'),
'pretix.event.category.deleted': _('The category has been deleted.'),
'pretix.event.category.changed': _('The category has been changed.'),
'pretix.event.category.reordered': _('The category has been reordered.'),
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been changed.'),
'pretix.event.question.reordered': _('The question has been reordered.'),
'pretix.event.discount.added': _('The discount has been added.'),
'pretix.event.discount.deleted': _('The discount has been deleted.'),
'pretix.event.discount.changed': _('The discount has been changed.'),

View File

@@ -80,7 +80,7 @@ class PermissionMiddleware:
"user.settings.2fa.disable",
"user.settings.2fa.regenemergency",
"user.settings.2fa.confirm.totp",
"user.settings.2fa.confirm.u2f",
"user.settings.2fa.confirm.webauthn",
"user.settings.2fa.delete",
"auth.logout",
"user.reauth"

View File

@@ -578,6 +578,16 @@ def get_organizer_navigation(request):
'children': children,
})
if request.organizer.settings.reusable_media_active:
nav.append({
'label': _('Reusable media'),
'url': reverse('control:organizer.reusable_media', kwargs={
'organizer': request.organizer.slug
}),
'icon': 'key',
'active': 'organizer.reusable_medi' in url.url_name,
})
if 'can_change_organizer_settings' in request.orgapermset:
nav.append({
'label': _('Devices'),

View File

@@ -46,4 +46,5 @@
{% endif %}
</div>
</form>
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
{% endblock %}

View File

@@ -16,6 +16,42 @@
{% endif %}
</small>
</h1>
<div class="helper-space-below">
{% trans "Shop URL:" %}
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
<div class="btn-group helper-display-inline-block">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="png" %}" target="_blank" download>
{% blocktrans with filetype="PNG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="svg" %}" target="_blank" download>
{% blocktrans with filetype="SVG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="jpeg" %}" target="_blank" download>
{% blocktrans with filetype="JPG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="gif" %}" target="_blank" download>
{% blocktrans with filetype="GIF" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
</ul>
</div>
<div class="clearfix"></div>
</div>
{% if has_overpaid_orders %}
<div class="alert alert-warning">
{% blocktrans trimmed %}

View File

@@ -118,7 +118,7 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_subject_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_subject_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_subject_order_placed_require_approval,mail_text_order_placed_require_approval,mail_subject_order_approved,mail_text_order_approved,mail_subject_order_approved_free,mail_text_order_approved_free,mail_subject_order_denied,mail_text_order_denied" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_subject_order_placed_require_approval,mail_text_order_placed_require_approval,mail_subject_order_approved,mail_text_order_approved,mail_send_order_approved_attendee,mail_subject_order_approved_attendee,mail_text_order_approved_attendee,mail_subject_order_approved_free,mail_text_order_approved_free,mail_send_order_approved_free_attendee,mail_subject_order_approved_free_attendee,mail_text_order_approved_free_attendee,mail_subject_order_denied,mail_text_order_denied" exclude="mail_send_order_approved_attendee,mail_send_order_approved_free_attendee"%}
</div>
<h4>{% trans "Attachments" %}</h4>
{% bootstrap_field form.mail_attachment_new_order layout="control" %}

View File

@@ -15,6 +15,7 @@
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_form_errors sform %}
<div class="tabbed-form">
<fieldset>
<legend>{% trans "Basics" %}</legend>

View File

@@ -4,6 +4,7 @@
{% load formset_tags %}
{% block inside %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% bootstrap_form_errors form layout="control" %}
{% csrf_token %}
<div class="row">
<div class="col-xs-12 col-lg-10">
@@ -178,6 +179,12 @@
<fieldset>
<legend>{% trans "Tickets & Badges" %}</legend>
{% bootstrap_field form.generate_tickets layout="control" %}
{% if form.media_policy %}
{% bootstrap_field form.media_policy layout="control" %}
{% endif %}
{% if form.media_type %}
{% bootstrap_field form.media_type layout="control" %}
{% endif %}
{% for f in plugin_forms %}
{% if f.is_layouts %}
{% bootstrap_form f layout="control" %}

View File

@@ -44,6 +44,9 @@
{% bootstrap_field form.valid_datetime_min layout="control" %}
{% bootstrap_field form.valid_datetime_max layout="control" %}
</div>
<div id="valid-string">
{% bootstrap_field form.valid_string_length_max layout="control" %}
</div>
<div id="valid-file">
{% bootstrap_field form.valid_file_portrait layout="control" %}
</div>

View File

@@ -364,7 +364,7 @@
</div>
<div class="panel-body">
{% for line in items.positions %}
<div class="row-fluid product-row {% if line.canceled %}pos-canceled{% endif %}">
<div class="row-fluid product-row {% if line.canceled %}pos-canceled{% endif %} {% if line.item.require_approval and order.require_approval and order.status == 'n' %}bg-warning{% endif %}">
<div class="col-md-9 col-xs-6">
{% if line.addon_to %}
<span class="addon-signifier">+</span>
@@ -462,6 +462,14 @@
</dd>
</div>
{% endif %}
{% for m in line.linked_media.all %}
<div class="cart-icon-details">
<dd>
<span class="fa fa-key fa-fw" aria-hidden="true"></span>
<a href="{% url "control:organizer.reusable_medium" organizer=request.organizer.slug pk=m.pk %}">{{ m.identifier }}</a> <span class="text-muted">({{ m.get_type_display }})</span>
</dd>
</div>
{% endfor %}
{% if not line.canceled %}
<div class="position-buttons">
{% if line.generate_ticket %}

View File

@@ -21,8 +21,8 @@
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
<br>
<strong>{% trans "System URL:" %}</strong> {{ settings.SITE_URL }}<br>
<strong>{% trans "Token:" %}</strong> {{ device.initialization_token }}
<strong>{% trans "System URL:" %}</strong> <code>{{ settings.SITE_URL }}</code><br>
<strong>{% trans "Token:" %}</strong> <code>{{ device.initialization_token }}</code>
</li>
</ol>
</div>

View File

@@ -200,6 +200,70 @@
{% bootstrap_field sform.cookie_consent_dialog_button_yes layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_button_no layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Reusable media" %}</legend>
{% bootstrap_field sform.reusable_media_active layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">{% trans "Barcode media" %}</h4>
</div>
<div class="panel-body">
<p class="help-block">
{% blocktrans trimmed %}
A "barcode medium" can be any printed or digital representation of a barcode.
The medium will initially be created through the sale of a product that has a
media policy requiring such a medium as well as a ticket or badge layout that
includes the "Reusable Medium ID" as a QR code. Later, the same barcode may
be re-used during the sale of a different product.
{% endblocktrans %}
{% blocktrans trimmed %}
Barcode media can currently only be connected to tickets.
{% endblocktrans %}
{% blocktrans trimmed %}
This subsequent reuse of the barcode is currently only supported during POS sales.
{% endblocktrans %}
</p>
{% bootstrap_field sform.reusable_media_type_barcode layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_type_barcode.id_for_label }}">
{% bootstrap_field sform.reusable_media_type_barcode_identifier_length layout="control" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">{% trans "NFC UID-based" %}</h4>
</div>
<div class="panel-body">
<p class="help-block">
{% blocktrans trimmed %}
This medium type can work with almost any type of NFC chip. With this
option, only the UID of the NFC chip is used for identification.
{% endblocktrans %}
{% blocktrans trimmed %}
NFC media can currently only be connected to gift cards.
{% endblocktrans %}
</p>
<p class="help-block">
<span class="fa fa-warning text-warning"></span>
{% blocktrans trimmed %}
This method does not provide a high level of protection against abuse since it
is possible for malicious users to clone someone's chip with the same UID.
{% endblocktrans %}
</p>
{% bootstrap_field sform.reusable_media_type_nfc_uid layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_type_nfc_uid.id_for_label }}">
{% bootstrap_field sform.reusable_media_type_nfc_uid_autocreate_giftcard layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_type_nfc_uid_autocreate_giftcard.id_for_label }}">
{% bootstrap_field sform.reusable_media_type_nfc_uid_autocreate_giftcard_currency layout="control" %}
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Invoices" %}</legend>
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}

View File

@@ -0,0 +1,113 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load money %}
{% block title %}{% trans "Reusable media" %}{% endblock %}
{% block inner %}
<h1>
{% trans "Reusable media" %}
</h1>
{% if media|length == 0 and not filter_form.filtered %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No media have been created yet.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
</div>
{% else %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-6 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %}
</div>
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
</p>
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Identifier" context "reusable_media" %}
<a href="?{% url_replace request 'ordering' '-identifier' %}"><i
class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>{% trans "Media type" context "reusable_media" %}
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Connections" context "reusable_media" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for m in media %}
<tr>
<td>
<a href="{% url "control:organizer.reusable_medium" organizer=request.organizer.slug pk=m.pk %}">
{% if not m.active %}<strike>{% endif %}
<strong>{{ m.identifier }}</strong>
{% if not m.active %}</strike>{% endif %}
</a>
</td>
<td>
{{ m.get_type_display }}
</td>
<td>
{% if m.customer %}
<span class="helper-display-block">
<span class="fa fa-user fa-fw"></span>
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
{{ m.customer }}
</a>
</span>
{% endif %}
{% if m.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=m.linked_orderposition.order.event.slug organizer=request.organizer.slug code=m.linked_orderposition.order.code %}">
{{ m.linked_orderposition.order.code }}</a>-{{ m.linked_orderposition.positionid }}
</span>
{% endif %}
{% if m.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=m.linked_giftcard.id %}">
{{ m.linked_giftcard.secret }}</a>
</span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:organizer.reusable_medium" organizer=request.organizer.slug pk=m.pk %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
<i class="fa fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% block title %}
{% blocktrans trimmed with id=medium.identifier context "reusable_media" %}
Medium {{ id }}
{% endblocktrans %}
{% endblock %}
{% block inner %}
<h1>
{% blocktrans trimmed with id=medium.identifier context "reusable_media" %}
Medium {{ id }}
{% endblocktrans %}
</h1>
<div class="row">
<div class="col-md-10 col-xs-12">
<div class="panel panel-primary items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Details" %}
</h3>
</div>
<div class="panel-body">
<form action="" method="post">
{% csrf_token %}
<dl class="dl-horizontal">
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd><code>{{ medium.identifier }}</code></dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
{% trans "disabled" %}
{% elif medium.is_expired %}
{% trans "expired" %}
{% else %}
{% trans "active" %}
{% endif %}
</dd>
<dt>{% trans "Connections" context "reusable_media" %}</dt>
<dd>
{% if medium.customer %}
<span class="helper-display-block">
<span class="fa fa-user fa-fw"></span>
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
{{ medium.customer }}
</a>
</span>
{% endif %}
{% if medium.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
{{ medium.linked_giftcard.secret }}</a>
</span>
{% endif %}
</dd>
{% if medium.notes %}
<dt>{% trans "Notes" %}</dt>
<dd>{{ medium.notes }}</dd>
{% endif %}
</dl>
</form>
<div class="text-right">
<a href="{% url "control:organizer.reusable_medium.edit" organizer=request.organizer.slug pk=medium.pk %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
</div>
</div>
</div>
</div>
<div class="col-md-2 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Medium history" context "reusable_media" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=medium %}
</div>
</div>
</div>
{% endblock %}

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