Compare commits

...

196 Commits

Author SHA1 Message Date
Richard Schreiber
2f8e6a2a4b only auto-submit subevent-select when context var "auto_submit" is True 2021-03-30 08:52:44 +02:00
Richard Schreiber
c084b91ab3 added columns to filter-form 2021-03-24 21:05:45 +01:00
Martin Gross
8baaa0a8c6 Add select2-subevent picker; display subevent start time 2021-03-24 13:50:27 +01:00
Richard Schreiber
53070f5d4b Cart: fix call to del if attribute is unknown when rendering a form label 2021-03-22 17:48:29 +01:00
Richard Schreiber
5685a349ea fix code style issues - missing whitespace around = operator 2021-03-22 17:05:13 +01:00
Richard Schreiber
1af69d5c76 Cart: Hide attendee information if not provided 2021-03-22 16:38:10 +01:00
Richard Schreiber
adddc7a71e A11y: add role=group and labels to multi-widgets (#2006)
* add role=group aria-labelledby to multiwidgets

* remove for-attribute from parent-label for grouped inputs

* add aria-labels to PhoneNumber-fields

* add aria-label to name multi-inputs
2021-03-22 15:19:29 +01:00
Richard Schreiber
11f23c3fd2 [a11y] Improved form error messages, descriptive labels, focusable toggle-link (#2002) 2021-03-19 16:13:25 +01:00
Raphael Michel
954fece6cf Log view: Page size selector 2021-03-19 10:49:03 +01:00
Richard Schreiber
8ef6adc3d5 A11y: make toggle-link for "view other date" focusable 2021-03-19 08:00:41 +01:00
Aksh Gupta
88ba7ab53a Refactor code quality issues (#2001) 2021-03-16 19:13:02 +01:00
Raphael Michel
eae55e4b5a Widget: Support button with subevent but without items 2021-03-16 19:01:43 +01:00
Raphael Michel
5ae839f62e Security Profile: Allow badge layouts for POS 2021-03-16 19:01:43 +01:00
Raphael Michel
7314d32422 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Maarten van den Berg
97d6ae8e55 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Maarten van den Berg
13063cb9d2 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.3% (3993 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Raphael Michel
2792813d95 Widget: Fix possible redirect loop 2021-03-16 17:26:20 +01:00
Martin Gross
d6aeefdf09 Add force-reactivate checkbox to order (#1997) 2021-03-16 16:49:37 +01:00
Raphael Michel
13056ef477 Widget: Do not prefill field with 0 2021-03-16 16:46:39 +01:00
Raphael Michel
6e2b5eae9a Widget: Open iframe even on mobile (to prevent breakage in WkWebView) 2021-03-16 16:16:59 +01:00
Raphael Michel
4cfb10b254 Widget: Make close icon independent of system font 2021-03-16 12:50:19 +01:00
Raphael Michel
ebd336e8cb Use new red color everywhere 2021-03-16 12:17:54 +01:00
Richard Schreiber
1357b010de [a11y] add missing labels on voucher-input and fix input.focus when revealing voucher-input via JS (#1998) 2021-03-16 12:17:47 +01:00
Richard Schreiber
09b2e69178 [a11y] Increase contrast on some colors for WCAG conformance (#1996)
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2021-03-16 12:10:37 +01:00
Raphael Michel
5e34032821 Fix #256 -- Allow exact filtering of voucher tags 2021-03-15 16:16:49 +01:00
Richard Schreiber
46cee890f0 QuestionAnswer: Add UNIQUE keys on (orderposition, question) and (cartposition, question) (#1994) 2021-03-15 15:34:33 +01:00
Raphael Michel
4a2ac110b3 Voucher bulk creation: More efficient implementation and async task 2021-03-14 18:19:49 +01:00
Raphael Michel
7eefd3dc59 Recommend upper-case index on pretixbase_voucher.code 2021-03-14 18:04:19 +01:00
Raphael Michel
fdca62685c Revert "Update Django to 3.1 as well as other dependencies"
This reverts commit b3c9dca024.
2021-03-12 10:52:02 +01:00
Raphael Michel
7ae38b5e97 Fix TypeError during invoice creation 2021-03-12 10:51:50 +01:00
Raphael Michel
76e9093fea Fix email sending during tests 2021-03-11 22:46:07 +01:00
Raphael Michel
b3c9dca024 Update Django to 3.1 as well as other dependencies 2021-03-11 21:59:04 +01:00
Raphael Michel
f4710cf019 Add index to documentation 2021-03-11 21:43:27 +01:00
Raphael Michel
5f192fd0ce Remove order status from emails 2021-03-11 17:56:28 +01:00
Raphael Michel
a897f60fc5 Fix crash during copying of check-in rules 2021-03-11 12:43:33 +01:00
Raphael Michel
74107781ce Email context: Auto-set position_or_address if position is set 2021-03-10 16:18:46 +01:00
Raphael Michel
ad219df7cf Fix incorrect attribute parameter in thumbnailed_file_input 2021-03-10 16:14:06 +01:00
Raphael Michel
002ab4aa06 runperiodic: --exclude 2021-03-09 11:50:19 +01:00
Raphael Michel
a84a726185 Try to avoid race condition when sending emails 2021-03-09 09:06:14 +01:00
Raphael Michel
5f58b93c71 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-08 18:09:09 +01:00
Raphael Michel
3eaaf80c0a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-08 18:09:09 +01:00
Raphael Michel
3b5d811b27 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-03-08 17:40:02 +01:00
Raphael Michel
f0da2b7233 E-mails: add additional information on order positions 2021-03-08 16:50:38 +01:00
lapor-kris
d8d7440b52 Translated on translate.pretix.eu (Slovenian)
Currently translated at 27.1% (1085 of 4002 strings)

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

powered by weblate
2021-03-08 16:50:33 +01:00
Raphael Michel
a1ec9fceb0 Quota list exporter: Add subevent information 2021-03-08 14:47:41 +01:00
Raphael Michel
27ff73255b Add new fields to invoice model and API 2021-03-08 14:28:04 +01:00
Raphael Michel
bba103156c Allow to cancel an order without creating a cancellation invoice 2021-03-08 11:26:52 +01:00
Raphael Michel
f1a98b5c30 Add event_info_text parameters 2021-03-05 17:56:29 +01:00
Raphael Michel
405b3a22e1 Fix bug when changing quotas in subevent bulk editor 2021-03-05 13:05:04 +01:00
Raphael Michel
a51c2a36a6 SubEvent search: Fix inconsistent ordering 2021-03-05 11:57:24 +01:00
Richard Schreiber
8e00970f04 Waiting list: Add name and phone number (#1987)
* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 19.9% (799 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 21.6% (865 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 23.8% (955 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 26.3% (1051 of 3996 strings)

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

powered by weblate

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Revert more of de597ba86

* Paginate list of gift cards

* Change texts on order confirmation page if no attachments are sent

* Update locales

* Added translation on translate.pretix.eu (Sinhala)

* Added translation on translate.pretix.eu (Sinhala)

* Translated on translate.pretix.eu (Sinhala)

Currently translated at 0.4% (18 of 4002 strings)

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

powered by weblate

* Fix initial value of phone number

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* update API-description with null-fields

* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Fix initial value of phone number

* update API-description with null-fields

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* fixed isort on migration

Co-authored-by: lapor-kris <kristijan.tkalec@posteo.si>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: helabasa <R45XvezA@pm.me>
Co-authored-by: Raphael Michel <michel@rami.io>
2021-03-05 10:02:37 +01:00
Raphael Michel
8ca2fe7707 Stripe: Deal with conflicting settings 2021-03-04 20:13:10 +01:00
Raphael Michel
b93e2307d0 CachedFileField: Prevent double upload leading to empty file 2021-03-04 18:11:11 +01:00
Raphael Michel
97f3b72254 CachedFileInput: Fix links to download file 2021-03-04 14:54:11 +01:00
Panawat Wong-kleaw
00a77d3de9 Manual payment: Add amount placeholder (#1990) 2021-03-03 15:04:44 +01:00
helabasa
35d9a0dacf Translated on translate.pretix.eu (Sinhala)
Currently translated at 0.4% (18 of 4002 strings)

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

powered by weblate
2021-03-03 12:53:23 +01:00
helabasa
d2e6320e1e Added translation on translate.pretix.eu (Sinhala) 2021-03-03 12:53:23 +01:00
helabasa
671eb902a8 Added translation on translate.pretix.eu (Sinhala) 2021-03-03 12:53:23 +01:00
Raphael Michel
be67059099 Update locales 2021-03-02 18:42:39 +01:00
Raphael Michel
6e3791a49e Change texts on order confirmation page if no attachments are sent 2021-03-02 18:36:00 +01:00
Raphael Michel
e3bd665093 Paginate list of gift cards 2021-03-02 18:31:05 +01:00
Raphael Michel
748e2bb2fa Revert more of de597ba86 2021-03-02 18:26:01 +01:00
lapor-kris
b13b34f00d Translated on translate.pretix.eu (Slovenian)
Currently translated at 26.3% (1051 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris
641e3216d9 Translated on translate.pretix.eu (Slovenian)
Currently translated at 23.8% (955 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris
c70901c129 Translated on translate.pretix.eu (Slovenian)
Currently translated at 21.6% (865 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris
460d39b8c2 Translated on translate.pretix.eu (Slovenian)
Currently translated at 19.9% (799 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
Raphael Michel
a9963aead1 Fix import order 2021-03-02 10:54:11 +01:00
Raphael Michel
a09dac89c4 Partially revert de597ba86 2021-03-02 09:30:27 +01:00
Raphael Michel
8af91b691d Allow configurable addition to the order confirmation message 2021-03-01 18:28:08 +01:00
Raphael Michel
2221b57dc9 Allow to disable ticket attachments to emails 2021-03-01 18:21:12 +01:00
Raphael Michel
8d99388c08 InvoiceExporter: Useful error message if PDF generation fails 2021-03-01 10:35:53 +01:00
Raphael Michel
de597ba864 Fix #1982 -- Stricter cleaning of dynamic values in invoices 2021-03-01 10:35:02 +01:00
Raphael Michel
2d9a16e94d Bump to 3.17.0.dev0 2021-02-26 17:48:59 +01:00
Raphael Michel
a0026d8a0c Bump to 3.16.0 2021-02-26 17:48:02 +01:00
Raphael Michel
1cee082821 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (3996 of 3996 strings)

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

powered by weblate
2021-02-26 17:47:56 +01:00
Raphael Michel
6c1a3a4c68 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3996 of 3996 strings)

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

powered by weblate
2021-02-26 17:47:56 +01:00
Raphael Michel
156e8413f8 Add geo to wordlist.txt 2021-02-26 17:43:00 +01:00
Raphael Michel
46ccce439a PDF: Add placeholder for the event name even in series 2021-02-26 17:39:18 +01:00
Richard Schreiber
675de12a5d Geo fields: only confirm/overwrite if new lat/lon differ from existing coordinates 2021-02-26 15:20:14 +01:00
Raphael Michel
5992892035 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-02-26 10:06:30 +01:00
Richard Schreiber
1c81792cd7 Geo fields: Allow overriding existing values (#1978) 2021-02-26 09:55:23 +01:00
Raphael Michel
73e7d407cd Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (3992 of 3992 strings)

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

powered by weblate
2021-02-26 09:48:15 +01:00
Raphael Michel
fa78583cd3 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3992 of 3992 strings)

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

powered by weblate
2021-02-26 09:48:15 +01:00
Raphael Michel
bcba7b70ca Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (3992 of 3992 strings)

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

powered by weblate
2021-02-26 09:48:15 +01:00
Raphael Michel
141c6d04b2 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3992 of 3992 strings)

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

powered by weblate
2021-02-26 09:48:15 +01:00
Raphael Michel
9c0da900a2 Update issue templates 2021-02-25 20:55:12 +01:00
Raphael Michel
580479b266 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-02-25 20:14:50 +01:00
Raphael Michel
4adaa2059d Clarify language 2021-02-25 17:23:00 +01:00
Raphael Michel
a900f39121 Check-in list update view: Fix incorrect timezone handling in exit_at_all 2021-02-25 12:32:55 +01:00
Richard Schreiber
b625d987a9 fix encoding issue in geocode-API call 2021-02-24 17:52:55 +01:00
Richard Schreiber
71e7d527d1 Merge pull request #1933 from pretix/a11y-add-landmarks
a11y: add landmarks, add missing labels, aria-hide icons, add checkout notifications to page title
2021-02-24 16:51:09 +01:00
Richard Schreiber
6fd0880e79 Change test, so at least body-element is available and ticket not in there 2021-02-24 16:09:30 +01:00
Richard Schreiber
8ca253c860 Fixed selectors in tests 2021-02-24 15:02:39 +01:00
Richard Schreiber
63c2852668 Merge branch 'master' into a11y-add-landmarks 2021-02-24 13:17:24 +01:00
Richard Schreiber
5b36fa198d Bulk action improvements: buttons (wording, color, icons, disabled-state), hide select-on-all-pages if only one results-page (#1973) 2021-02-24 09:59:07 +01:00
Irmantas
ef8b6f60b8 Adjust runperiodic logging (#1974)
Co-authored-by: Irmantas Marozas <irmantas.marozas@juvare.com>
2021-02-23 15:47:13 +01:00
Richard Schreiber
6ca07662b6 Merge branch 'master' into a11y-add-landmarks 2021-02-23 11:58:18 +01:00
Richard Schreiber
45a499ebba Merge pull request #1931 from pretix/bulk-select-with-drag-over
add bulk selection by click and drag over table rows
2021-02-22 22:00:40 +01:00
Richard Schreiber
1bfa4c6fda update toggle-state after release/pointerup instead of during updateSelection 2021-02-22 18:16:46 +01:00
Richard Schreiber
8a169d0496 fix bug when releasing outside of table 2021-02-22 18:13:18 +01:00
Richard Schreiber
40dbae76ca remove call to console.log 2021-02-22 17:47:03 +01:00
Richard Schreiber
4203087eff removed .warning from selected $rows as it interferes with .table-select-all 2021-02-22 17:46:04 +01:00
Richard Schreiber
88bf31bd7a Merge branch 'master' into bulk-select-with-drag-over 2021-02-22 16:37:39 +01:00
Richard Schreiber
3423923d84 Reduced "tickets and/or products" to "products" 2021-02-22 16:02:45 +01:00
Raphael Michel
beb33e21ee Merge pull request #1927 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2021-02-22 15:24:59 +01:00
Ondřej Sokol
461ab8ba0a Translated on translate.pretix.eu (Czech)
Currently translated at 4.1% (162 of 3940 strings)

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

powered by weblate
2021-02-22 15:22:44 +01:00
Raphael Michel
7562f333cf Subevents: Bulk editor (#1918)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2021-02-22 15:22:40 +01:00
Richard Schreiber
32f1c32936 reverted source order for lang-nav, kept as nav-element 2021-02-22 14:59:24 +01:00
Raphael Michel
eb0123e350 Allow to inspect organizer-level logs 2021-02-22 10:15:59 +01:00
Raphael Michel
37ba885c55 Check-in rule editor: Set tolerance to 0 when using custom time 2021-02-19 18:12:30 +01:00
Raphael Michel
8330448a94 Fix import order 2021-02-19 16:13:32 +01:00
Raphael Michel
8582bf8158 Use footernav block on organizer page 2021-02-19 16:00:12 +01:00
Richard Schreiber
e872180ed1 add order confirmation to <title> 2021-02-19 12:00:03 +01:00
Richard Schreiber
cc88e70db6 hide reservation timer from screen readers 2021-02-19 11:59:47 +01:00
Richard Schreiber
c335dd35b3 moved notice-bottom in footer, added footernav 2021-02-19 11:58:56 +01:00
Richard Schreiber
cea8efc4a3 added <main> and <aside> to checkout pages 2021-02-19 11:57:45 +01:00
Richard Schreiber
c6c0f92891 moved update()-event to checkboxes’ change-event, added row-highlight if selected 2021-02-19 11:03:47 +01:00
Richard Schreiber
d5950821e2 optimized update() to only check the least number of checkboxes 2021-02-19 11:02:42 +01:00
Richard Schreiber
78f2581bb8 added labels to batch-select checkboxes 2021-02-19 11:00:38 +01:00
Richard Schreiber
c9f89dc920 simplified selection algorithm 2021-02-19 07:33:01 +01:00
Richard Schreiber
fb7d38ede0 add bulk selection by click and drag over table rows 2021-02-18 21:05:30 +01:00
Raphael Michel
8be2f9ad6b XLSX exports: Strip all illegal characters 2021-02-17 17:34:08 +01:00
Richard Schreiber
c033efbfa2 Merge pull request #1929 from pretix/fix-add-line-breaks-to-voucher-list
Voucher creation: Add linebreaks to {voucher_list} in emails
2021-02-17 15:35:26 +01:00
Richard Schreiber
d990f0e927 fix markdown-linebreaks for voucher_list 2021-02-17 15:23:16 +01:00
Richard Schreiber
e011b7810d Merge pull request #1928 from pretix/mark-as-paid-add-notify-checkbox
Add checkbox to disable email sending when marking an order as paid
2021-02-17 14:58:35 +01:00
Richard Schreiber
0d0bbe1ce5 add send_email field to mark-paid 2021-02-17 12:37:26 +01:00
Raphael Michel
488273d5f2 Fix #1912 -- Auto-open all <details> with an error inside 2021-02-15 18:30:37 +01:00
Raphael Michel
9fdaf040dc Asynctask JS: On errors, only replace inner part of page 2021-02-15 18:30:37 +01:00
Raphael Michel
d109dde1e1 Fix form validation of exporters (again) 2021-02-15 18:30:37 +01:00
Raphael Michel
d713398e88 Merge pull request #1925 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2021-02-15 18:02:34 +01:00
Jaakko Rinta-Filppula
0898d13e4c Translated on translate.pretix.eu (Finnish)
Currently translated at 18.5% (731 of 3940 strings)

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

powered by weblate
2021-02-15 17:48:50 +01:00
Raphael Michel
04098ce002 API: Add docs on order lifecycle 2021-02-15 17:48:43 +01:00
Richard Schreiber
f2a18325b6 Add a voucher’s comment to voucher.csv download (#1926) 2021-02-15 13:41:44 +01:00
Raphael Michel
4db0530c09 REST API docs: Remove "versionchanged" notes older than ~1 year (version 3.2 and below) 2021-02-15 09:43:58 +01:00
Raphael Michel
938d84b251 Update order state chart 2021-02-15 09:38:13 +01:00
Raphael Michel
c65b2aa4f8 API: Add missing field SubEvent.frontpage_text 2021-02-15 09:16:40 +01:00
Richard Schreiber
2583e6166a Merge branch 'master' into a11y-add-landmarks 2021-02-12 20:40:24 +01:00
Richard Schreiber
825fd1820b [a11y] Add text "required" to label of required inputs (#1923) 2021-02-12 14:05:30 +01:00
Raphael Michel
c8d039b196 Sendmail: Allow to filter by order date 2021-02-12 12:41:45 +01:00
Raphael Michel
72b6ff0389 Sendmail form: Fix validation problems 2021-02-12 12:34:04 +01:00
Richard Schreiber
ef4db07e8b omit ? in lang-nav redirect when not needed 2021-02-11 17:27:05 +01:00
Richard Schreiber
ef1e5759eb marked checkout-steps as completed/current
See https://www.w3.org/WAI/tutorials/forms/multi-page/
2021-02-11 17:26:14 +01:00
Richard Schreiber
9f1079dcc4 checkout-flow steps to page-title 2021-02-11 17:25:31 +01:00
Richard Schreiber
518c1fbbf2 Added notifications/messages to page-title 2021-02-11 17:24:05 +01:00
Richard Schreiber
b9c9a03cdd Fixed item.name for price input 2021-02-11 16:16:17 +01:00
Richard Schreiber
5060bac7e0 update aria-label on <main> 2021-02-11 16:04:07 +01:00
Richard Schreiber
c4be508e26 added anchor link to voucher-input 2021-02-11 16:03:47 +01:00
Richard Schreiber
c75f741d4f removed variations-toggle hreef-attribute to enable non-JS dialog-open behaviour 2021-02-11 16:03:13 +01:00
Richard Schreiber
d6ef563f83 added labels (price input, image-lightbox link) 2021-02-11 16:02:18 +01:00
Richard Schreiber
3f75a935a3 changed products to <article> 2021-02-10 18:03:30 +01:00
Richard Schreiber
246e7c9443 added aria-labels to category-sections 2021-02-10 18:03:09 +01:00
Richard Schreiber
3dd685bf7a Updated main aria-label 2021-02-10 17:55:30 +01:00
Richard Schreiber
1480bd0690 added aria-hidden to fontawesome and some aria-label if needed 2021-02-10 17:27:49 +01:00
Richard Schreiber
01af8568ca converted second lang-nav to <nav> and changed source order 2021-02-10 16:41:59 +01:00
Richard Schreiber
74461dde50 added landmarks to startpage 2021-02-10 16:36:31 +01:00
Raphael Michel
f0fd4272dc Add more features to custom meta properties (#1922) 2021-02-10 11:01:25 +01:00
Raphael Michel
a0f60c71b9 Add order time to check-in list CSV export 2021-02-10 09:11:16 +01:00
Raphael Michel
6b2ab44b26 Fix undefined variable 2021-02-09 19:00:20 +01:00
Raphael Michel
9472d81e55 Invalidate ticket cache after a change in events or subevents 2021-02-09 18:33:04 +01:00
Raphael Michel
b630174f72 Fix bug when modifying an order with an address in a country with a state 2021-02-09 18:23:24 +01:00
Raphael Michel
25c35b0f73 Docker: Log more things to stdout 2021-02-09 18:16:35 +01:00
Raphael Michel
c0792f4171 Fallback to random ticket secret generator if invalid one is selected 2021-02-09 16:01:33 +01:00
Raphael Michel
5d490728df Check-in: Do not respond with outdated question answers in pdf_data 2021-02-09 12:55:27 +01:00
Raphael Michel
21fbf095cf Fix compatibility with cryptography 3.4.x 2021-02-08 18:01:05 +01:00
Raphael Michel
7b8ad1ebbe Remove --no-use-pep517 2021-02-08 17:56:05 +01:00
Raphael Michel
81f37d9ce5 PDF layout: Allow to show photos from questions (#1919) 2021-02-08 17:48:06 +01:00
Raphael Michel
40c4872459 Check-in: Save answers independent of result 2021-02-08 17:01:55 +01:00
Martin Gross
f0574755a2 Add salutation_given_family to PERSON_NAME_SCHEMES. (#1921) 2021-02-05 13:31:57 +01:00
Raphael Michel
4cfedebf3b Fix scoping issue in tests 2021-02-05 12:53:55 +01:00
Raphael Michel
45376dd757 Fix linter issue 2021-02-04 21:43:04 +01:00
Raphael Michel
0999f41b0c Do not allow modifications after checkin 2021-02-04 21:43:04 +01:00
Raphael Michel
565f77d13b Add imprint and contact mail on organizer level 2021-02-04 17:36:29 +01:00
Raphael Michel
5ae7a350b0 Add signal global_footer_link 2021-02-04 17:32:10 +01:00
Raphael Michel
af7d9942f6 Sort payment providers by public name 2021-02-04 17:21:47 +01:00
Raphael Michel
36efb25b98 Invoice address: Always validate that VAT ID is for correct country 2021-02-04 17:21:24 +01:00
Raphael Michel
7a496da945 Widget: Introduce disable-iframe parameter 2021-02-04 10:58:15 +01:00
Raphael Michel
03f1016cc7 Fix syntax error in setup.py 2021-02-04 10:42:41 +01:00
Raphael Michel
4d4d2d5fe7 Fix ill-formed requirement 2021-02-04 10:24:04 +01:00
Raphael Michel
98f48e78a8 Update bleach to 3.3.0 2021-02-04 10:23:18 +01:00
Raphael Michel
512c9f5301 Upgrade bootstrap JS to 3.4.1 2021-02-04 10:20:49 +01:00
Raphael Michel
d16c59e86c Update Vue.js from 2.4.0 to 2.6.12 2021-02-04 10:06:51 +01:00
Raphael Michel
177b0505fd Update moment.js from 2.14 to 2.29 2021-02-04 10:06:38 +01:00
Raphael Michel
01f7a70347 ADd some extensibility features to MultiSheetListExporter 2021-02-03 17:29:15 +01:00
Raphael Michel
3b4c99d450 Rich text: Fix issue with <a> without href="" 2021-02-02 15:58:43 +01:00
Raphael Michel
3bb23bb77e Invoice exporter: Add order ID to filename 2021-02-02 15:54:05 +01:00
Raphael Michel
89da0847ca Device security policy: Allow order payments for POS 2021-02-02 15:54:05 +01:00
Martin Gross
07bed72b5e Add information on `subevent`-parameter for pretix-Button 2021-02-01 16:08:16 +01:00
Raphael Michel
c103288eec 2021 attempt at disabling autocomplete in date fields
Apparently, we so far disabled "autofill" but not "autocomplete". For
date fields, autocomplete is more relevant. Explanation
https://stackoverflow.com/a/57810447/336784
2021-02-01 10:07:57 +01:00
Raphael Michel
03648b77b1 List of teams: Order alphabetically 2021-02-01 10:07:57 +01:00
Raphael Michel
818d75ddd7 Merge pull request #1917 from pretix-translations/weblate-pretix-pretix 2021-01-30 21:02:25 +01:00
Raphael Michel
20f608caae Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3940 of 3940 strings)

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

powered by weblate
2021-01-30 20:49:41 +01:00
Raphael Michel
7b3a6d47fc Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (3940 of 3940 strings)

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

powered by weblate
2021-01-30 20:49:26 +01:00
Maarten van den Berg
d586406c79 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3940 of 3940 strings)

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

powered by weblate
2021-01-29 12:23:33 +01:00
Maarten van den Berg
e214c8cb95 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3940 of 3940 strings)

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

powered by weblate
2021-01-29 12:23:31 +01:00
Raphael Michel
81f2b9db30 Fix typo in docstring 2021-01-29 10:32:35 +01:00
Raphael Michel
04a6ed20b9 Bump to 3.16.0.dev0 2021-01-29 10:14:45 +01:00
289 changed files with 127704 additions and 80971 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Bug report
about: Please only create issues for bug reports. Feature requests or general questions
should start as a "Discussion" on GitHub.
title: ''
labels: ''
assignees: ''
---
<!-- Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub. -->
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
run: pip3 install -Ur src/requirements/dev.txt
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -49,7 +49,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

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

View File

@@ -2,9 +2,8 @@
file=/tmp/supervisor.sock
[supervisord]
logfile=/tmp/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
logfile=/dev/stdout
logfile_maxbytes=0
loglevel=info
pidfile=/tmp/supervisord.pid
nodaemon=false

View File

@@ -3,5 +3,7 @@ command=/usr/sbin/nginx
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

View File

@@ -4,3 +4,7 @@ autostart=true
autorestart=true
priority=5
user=pretixuser
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

View File

@@ -5,3 +5,7 @@ autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

View File

@@ -60,6 +60,10 @@ Here is the currently recommended set of commands::
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
ON pretixbase_invoiceaddress
USING gin (upper("company") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email_upper
ON public.pretixbase_orderposition (upper((attendee_email)::text));
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code_upper
ON public.pretixbase_voucher (upper((code)::text));
Also, if you use our ``pretix-shipping`` plugin::

View File

@@ -8,4 +8,5 @@ This part of the documentation contains how-to guides on some special use cases
.. toctree::
:maxdepth: 2
order_lifecycle
custom_checkout

View File

@@ -0,0 +1,56 @@
Understanding the life cycle of orders
======================================
When integrating pretix with other systems, it is important that you understand how orders and related objects
such as order positions, fees, payments, refunds, and invoices work together, in order to react to their changes
properly and map them to processes in your system.
Order states
------------
Generally, an order can be in six states. For compatibility reasons, the ``status`` field only allows four values
and the two remaining states are modeled through the ``require_approval`` field and the number of positions within
an order. The states and their allowed changes are shown in the following graph:
.. image:: /images/order_states.png
Object types
------------
Order
One order represents one purchase. It's the main object you interact with and bundles all the other objects
together. Orders can change in many ways during their lifetime, but will never be deleted (unless ``testmode``
is set to ``true``).
Order position
An order position represents one product contained in the order. Orders can usually have multiple positions.
There might be a parent-child relation between order positions if one position is an add-on to another position.
Order positions can change in many ways during their lifetime, and can also be removed or added to an order.
Order fees
A fee represents a charge that is not related to a product. Examples include shipping fees, service fees, and
cancellation fees.
Order fees can change in many ways during their lifetime, and can also be removed or added to an order.
Order payment
An order payment represents one payment attempt with a specific payment method and amount. An order can have
multiple payments attached.
Order payments have their own state diagram. Apart from their state and their meta information (e.g. used
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
Order refund
An order payment represents one refund attempt with a specific payment method and amount. An order can have
multiple refunds attached.
Order refunds have their own state diagram. Apart from their state and their meta information (e.g. used
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
Invoice
An invoice represents a legal document stating the contents of an order. While the backend technically allows
to update an invoice in some situations, invoices are generally considered immutable. Once they are issued,
they no longer change. If the order changes substantially (e.g. prices change), an invoice is canceled through
creation of a new invoice with the opposite amount, plus the issuance of a new invoice.
Here's an example of how they all play together:
.. image:: /images/order_objects.png

View File

@@ -42,10 +42,6 @@ seat objects The assigned se
└ seat_guid string Identifier of the seat within the seating plan
===================================== ========================== =======================================================
.. versionchanged:: 1.17
This resource has been added.
.. versionchanged:: 3.0
This ``seat`` attribute has been added.

View File

@@ -25,14 +25,6 @@ is_addon boolean If ``true``, it
defining add-ons for other products.
===================================== ========================== =======================================================
.. versionchanged:: 1.14
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 1.16
The field ``internal_name`` has been added.
Endpoints
---------

View File

@@ -36,22 +36,6 @@ rules object Custom check-in
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
This resource has been added.
.. versionchanged:: 1.11
The ``positions`` endpoints have been added.
.. versionchanged:: 1.13
The ``include_pending`` field has been added.
.. versionchanged:: 3.2
The ``auto_checkin_sales_channels`` field has been added.
.. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
@@ -68,10 +52,6 @@ exit_all_at datetime Automatically c
Endpoints
---------
.. versionchanged:: 1.15
The ``../status/`` detail endpoint has been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
Returns a list of all check-in lists within a given event.
@@ -380,29 +360,6 @@ Endpoints
Order position endpoints
------------------------
.. versionchanged:: 1.15
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
``order__status__in``, ``subevent__in``, ``addon_to__in``, and ``search``. The search for attendee names and order
codes is now case-insensitive.
The ``.../redeem/`` endpoint has been added.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. versionchanged:: 2.7
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. versionchanged:: 3.2
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
automatically by the system.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as

View File

@@ -52,31 +52,6 @@ sales_channels list A list of sales
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
.. versionchanged:: 1.15
The ``plugins`` field has been added.
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 2.1
Filters have been added to the list of events.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added.
.. versionchanged:: 2.8
When cloning events, the ``testmode`` attribute will now be cloned, too.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.

View File

@@ -15,8 +15,24 @@ number string Invoice number
order string Order code of the order this invoice belongs to
is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice.
invoice_from string Sender address
invoice_to string Receiver address
invoice_from_name string Sender address: Name
invoice_from string Sender address: Address lines
invoice_from_zipcode string Sender address: ZIP code
invoice_from_city string Sender address: City
invoice_from_country string Sender address: Country code
invoice_from_tax_id string Sender address: Local Tax ID
invoice_from_vat_id string Sender address: EU VAT ID
invoice_to string Full recipient address
invoice_to_company string Recipient address: Company name
invoice_to_name string Recipient address: Person name
invoice_to_street string Recipient address: Address lines
invoice_to_zipcode string Recipient address: ZIP code
invoice_to_city string Recipient address: City
invoice_to_state string Recipient address: State (only used in some countries)
invoice_to_country string Recipient address: Country code
invoice_to_vat_id string Recipient address: EU VAT ID
invoice_to_beneficiary string Invoice beneficiary
custom_field string Custom invoice address field
date date Invoice date
refers string Invoice number of an invoice this invoice refers to
(for example a cancellation refers to the invoice it
@@ -30,6 +46,31 @@ footer_text string Text to be prin
lines list of objects The actual invoice contents
├ position integer Number of the line within an invoice.
├ description string Text representing the invoice line (e.g. product name)
├ item integer Product used to create this line. Note that everything
about the product might have changed since the creation
of the invoice. Can be ``null`` for all invoice lines
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ variation integer Product variation used to create this line. Note that everything
about the product might have changed since the creation
of the invoice. Can be ``null`` for all invoice lines
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ event_date_from datetime Start date of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees).
├ event_date_to datetime End date of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no end date set.
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
name was set or if names are configured to not be added to invoices.
├ gross_value money (string) Price including taxes
├ tax_value money (string) Tax amount included
├ tax_name string Name of used tax rate (e.g. "VAT")
@@ -46,28 +87,16 @@ internal_reference string Customer's refe
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
number.
.. versionchanged:: 1.7
The attributes ``lines.tax_name``, ``foreign_currency_display``, ``foreign_currency_rate``, and
``foreign_currency_rate_date`` have been added.
.. versionchanged:: 1.9
The attribute ``internal_reference`` has been added.
.. versionchanged:: 3.4
The attribute ``lines.number`` has been added.
.. versionchanged:: 3.17
The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
``refers`` now returns an invoice number including the prefix.
Endpoints
---------
@@ -101,8 +130,24 @@ Endpoints
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
"invoice_from_name": "Big Events LLC",
"invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
"invoice_to_city": "Testington",
"invoice_to_state": null,
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"custom_field": null,
"date": "2017-12-01",
"refers": null,
"locale": "en",
@@ -115,6 +160,11 @@ Endpoints
{
"position": 1,
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",
@@ -166,8 +216,24 @@ Endpoints
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
"invoice_from_name": "Big Events LLC",
"invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
"invoice_to_city": "Testington",
"invoice_to_state": null,
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"custom_field": null,
"date": "2017-12-01",
"refers": null,
"locale": "en",
@@ -180,6 +246,11 @@ Endpoints
{
"position": 1,
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",

View File

@@ -28,10 +28,6 @@ multi_allowed boolean Adding the same
price_included boolean Adding this add-on to the item is free
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -30,10 +30,6 @@ designated_price money (string) Designated pric
taxation. This is not added to the price.
===================================== ========================== =======================================================
.. versionchanged:: 2.6
This resource has been added.
Endpoints
---------

View File

@@ -26,14 +26,6 @@ description multi-lingual string A public descri
position integer An integer, used for sorting
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added.
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -118,44 +118,6 @@ bundles list of objects Definition of b
meta_data object Values set for event-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added for ``variations``.
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
``checkin_attention`` has been added.
.. versionchanged:: 1.12
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
The attribute ``price_included`` has been added to ``addons``.
.. versionchanged:: 1.16
The ``internal_name`` and ``original_price`` fields have been added.
.. versionchanged:: 2.0
The field ``require_approval`` has been added.
.. versionchanged:: 2.3
The ``sales_channels`` attribute has been added.
.. versionchanged:: 2.4
The ``generate_tickets`` attribute has been added.
.. versionchanged:: 2.6
The ``bundles`` and ``require_bundling`` attributes have been added.
.. versionchanged:: 3.0
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
.. versionchanged:: 3.7
The attribute ``meta_data`` has been added.

View File

@@ -94,60 +94,6 @@ last_modified datetime Last modificati
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
a custom text might still be returned.
.. versionchanged:: 1.7
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
deprecated in favor of the new ``fees`` attribute but will still be served and removed in 1.9.
.. versionchanged:: 1.9
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
The attribute ``invoice_address.internal_reference`` has been added.
.. versionchanged:: 1.13
The field ``checkin_attention`` has been added.
.. versionchanged:: 1.15
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
``order.payment_fee_tax_rule`` have finally been removed.
.. versionchanged:: 1.16
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
.. versionchanged:: 2.0
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. versionchanged:: 2.3
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. versionchanged:: 3.5
The ``order.fees.canceled`` attribute has been added.
@@ -220,7 +166,7 @@ downloads list of objects List of ticket
└ url string Download URL
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer
├ answer string Text representation of the answer (URL if answer is a file)
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
@@ -233,30 +179,6 @@ pdf_data object Data object req
``pdf_data=true`` query parameter to your request.
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added.
.. versionchanged:: 1.11
The attribute ``checkins.list`` has been added.
.. versionchanged:: 1.14
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
.. versionchanged:: 1.16
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
.. versionchanged:: 3.0
The attribute ``seat`` has been added.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. versionchanged:: 3.3
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
@@ -274,6 +196,10 @@ pdf_data object Data object req
The ``checkin.type`` attribute has been added.
.. versionchanged:: 3.16
Answers to file questions are now returned as an URL.
.. _order-payment-resource:
Order payment resource
@@ -302,14 +228,6 @@ details object Payment-specifi
the object is empty.
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. versionchanged:: 3.1
The attributes ``payment_url`` and ``details`` have been added.
.. _order-refund-resource:
Order refund resource
@@ -330,17 +248,9 @@ execution_date datetime Date and time o
provider string Identification string of the payment provider
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
List of all orders
------------------
.. versionchanged:: 1.15
Filtering for emails or order codes is now case-insensitive.
.. versionchanged:: 3.5
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
@@ -1446,21 +1356,6 @@ Sending e-mails
List of all order positions
---------------------------
.. versionchanged:: 1.15
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
``order__status__in``, ``subevent__in``, ``addon_to__in`` and ``search``. The search for attendee names and order
codes is now case-insensitive.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. versionchanged:: 3.5
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
@@ -1800,10 +1695,6 @@ Manipulating individual positions
Order payment endpoints
-----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. versionchanged:: 3.6
Payments can now be created through the API.
@@ -2083,10 +1974,6 @@ Order payment endpoints
Order refund endpoints
----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Returns a list of all refunds for an order.

View File

@@ -19,10 +19,6 @@ identifier string An arbitrary st
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -75,28 +75,6 @@ dependency_value string An old version
for one value. **Deprecated.**
===================================== ========================== =======================================================
.. versionchanged:: 1.12
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
been added.
.. versionchanged:: 1.14
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
.. versionchanged:: 2.7
The attribute ``hidden`` and the question type ``CC`` have been added.
.. versionchanged:: 3.0
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
.. versionchanged:: 3.5
The attribute ``help_text`` has been added.

View File

@@ -30,14 +30,6 @@ release_after_exit boolean Whether the quo
have been scanned at an exit.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attributes ``close_when_sold_out`` and ``closed`` have been added.
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.

View File

@@ -20,10 +20,6 @@ layout object JSON representa
still evolves. The version in use can be found `here`_.
===================================== ========================== =======================================================
.. versionchanged:: 3.0
This endpoint has been added.
Endpoints
---------

View File

@@ -33,6 +33,7 @@ date_to datetime The sub-event's
date_admission datetime The sub-event's admission date (or ``null``)
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
frontpage_text multi-lingual string The description of the event (or ``null``)
location multi-lingual string The sub-event location (or ``null``)
geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``)
@@ -54,25 +55,6 @@ seat_category_mapping object An object mappi
last_modified datetime Last modification of this object
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
.. versionchanged:: 2.1
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
.. versionchanged:: 2.6
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 2.7
The attribute ``is_public`` has been added.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.

View File

@@ -24,14 +24,6 @@ home_country string Merchant countr
``null`` or empty string
===================================== ========================== =======================================================
.. versionchanged:: 1.7
This resource has been added.
.. versionchanged:: 1.9
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
Endpoints
---------

View File

@@ -46,14 +46,6 @@ show_hidden_items boolean Only if set to
===================================== ========================== =======================================================
.. versionchanged:: 1.9
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attribute ``show_hidden_items`` has been added.
.. versionchanged:: 3.4
The attribute ``seat`` has been added.

View File

@@ -13,7 +13,10 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the waiting list entry
created datetime Creation date of the waiting list entry
name string Name of the user on the waiting list (or ``null``)
name_parts object of strings Decomposition of name of the user (or ``null``)
email string Email address of the user on the waiting list
phone string Phone number of the user on the waiting list (or ``null``)
voucher integer Internal ID of the voucher sent to this user. If
this field is set, the user has been sent a voucher
and is no longer waiting. If it is ``null``, the

View File

@@ -34,7 +34,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
.. automodule:: pretix.presale.signals
@@ -79,7 +79,7 @@ Ticket designs
""""""""""""""
.. automodule:: pretix.base.signals
:members: layout_text_variables
:members: layout_text_variables, layout_image_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: override_layout

View File

@@ -82,11 +82,15 @@ Orders
^^^^^^
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
An order can be in one of currently four states that are listed in the diagram below:
An order can be in one of currently six states that are listed in the diagram below:
.. image:: /images/order_states.png
There are additional "fake" states that are displayed like states but not represented as states in the system:
The dotted lines represent status changes that usually do not happen as part of the regular process, but can be
performed manually in the admin backend.
For historical reasons, there are only four valid values of the ``status`` field, and the two additional states are
represented differently:
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,34 @@
@startuml
participant User
collections "OrderPayment\nOrderRefund" as P
collections "Order\nOrderPosition" as O
collections "Invoice\nInvoiceLine" as I
User -> O: Order placed (€100)
rnote over O #6DD96D: Order A1B2C\nstatus = **n**\ntotal = €100
O -> P: Payment created
O -> I: Invoice created\n(can also happen later)
rnote over I #6DD96D: Invoice 00001\n€100
rnote over P #6DD96D: OrderPayment A1B2C-P-1\nstate = **created**
P -> User: Payment details (web, email)
User -> P: Payment performed
rnote over P #EFF46B: OrderPayment A1B2C-P-1\nstate = **confirmed**
P -> O: Order marked as paid
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
User -> O: Data change (e.g. invoice address)
O -> I: Invoice reissued
rnote over I #6DD96D: Invoice 00002\n€-100
rnote over I #6DD96D: Invoice 00003\n€100
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
User -> O: Order canceled
rnote over O #EFF46B: Order A1B2C\nstatus = **c**
O -> I: Invoice canceled
rnote over I #6DD96D: Invoice 00004\n€-100
O -> P: Refund started
rnote over P #6DD96D: OrderRefund\nA1B2C-R-1\nstate = **created**
P -> User: Money sent
rnote over P #EFF46B: OrderRefund\nA1B2C-R-1\nstate = **done**
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,19 +1,39 @@
@startuml
Pending: Order is expecting payment\nOrder reduces quotas
Expired: Payment period is over\nOrder does not affect quotas
Paid: Order was successful\nOrder reduces quotas
Canceled: Order has been canceled\nOrder does not affect quotas
state "Approval Pending" as AP
state "Canceled (with paid fee)" as CP
AP: status = "n"
AP: require_approval = true
Pending: status = "n"
Pending: require_approval = false
Pending: Tickets reserved: yes
Expired: status = "e"
Expired: Tickets reserved: no
Paid: status = "p"
Paid: count(positions | !canceled) > 0
Paid: Tickets reserved: yes
CP: status = "p"
CP: count(positions | !canceled) = 0
Canceled: status = "c"
Canceled: Tickets reserved: no
[*] --> Pending: customer\nplaces order
Pending --> Paid: successful payment
Pending --> Expired: automatically\nor manually\non admin action
Expired --> Paid: if payment is received\nonly if quota left
Expired --> Canceled
Expired --> Pending: manually\non admin action
Paid --> Canceled: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
Pending --> Canceled: on admin or\ncustomer action
Paid -> Pending: manually on admin action
[*] --> Paid: customer\nplaces free order
[*] -> Pending: order placed\ntotal > 0
[*] -> Paid: order placed\ntotal = 0
[*] -> AP: order placed\napproval required
Pending --> Paid: order paid
Pending --> Expired: after payment\ndeadline
Expired --> Paid: order paid\n(only if quota left)
Expired -[dashed]-> Canceled
Expired -[dashed]-> Pending: order extended
Paid --> Canceled: order canceled
Pending --> Canceled: order canceled
Paid -[dashed]-> Pending: refund
AP --> Pending: order approved
AP --> Canceled: order denied
Paid --> CP: order canceled\n(with cancellation fee)
Canceled -[dashed]-> Pending: order reactivated
Canceled -[dashed]-> Paid: order reactivated
CP -[dashed]-> Canceled: fee canceled
@enduml

View File

@@ -22,10 +22,6 @@ item_assignments list of objects Products this l
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
Endpoints
---------

View File

@@ -24,14 +24,6 @@ item_assignments list of objects Products this l
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
.. versionchanged:: 2.3
The ``item_assignments.sales_channel`` field has been added.
Endpoints
---------

View File

@@ -88,6 +88,15 @@ website. If you confident to have a good reason for not using SSL, you can overr
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
Always open a new tab
---------------------
If you want the checkout process to always open a new tab regardless of screen size, you can pass the ``disable-iframe``
attribute::
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
Pre-selecting a voucher
-----------------------
@@ -197,7 +206,10 @@ should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,ite
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
items, if the items have variations. If you omit the ``items`` attribute, the general start page will be presented.
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
In case you are using an event-series, you will need to specify the subevent for which the item(s) should be put in the
cart. This can be done by specifying the ``subevent``-attribute.
Just as the widget, the button supports the optional attributes ``voucher``, ``disable-iframe``, and ``skip-ssl-check``.
You can style the button using the ``pretix-button`` CSS class.
@@ -425,11 +437,6 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
</script>
.. versionchanged:: 2.3
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
fully if you configured a redis server.
.. versionchanged:: 3.6
Dynamically opening the widget has been added in pretix 3.6.

View File

@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n
./manage.py collectstatic --noinput
@@ -23,3 +23,8 @@ test:
coverage:
coverage run -m py.test
npminstall:
mkdir -p pretix/static.dist/node_prefix
npm install --prefix=pretix/static.dist/node_prefix pretix/static/npm_dir/

View File

@@ -1 +1 @@
__version__ = "3.15.0"
__version__ = "3.17.0.dev0"

View File

@@ -10,7 +10,7 @@ class FullAccessSecurityProfile:
class AllowListSecurityProfile:
allowlist = tuple()
allowlist = ()
def is_allowed(self, request):
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
@@ -41,6 +41,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:order-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -68,6 +69,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -93,11 +95,15 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:order-mark_canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),

View File

@@ -13,7 +13,7 @@ from rest_framework.relations import SlugRelatedField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Event, TaxRule
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
@@ -174,9 +174,12 @@ class EventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
@@ -223,6 +226,14 @@ class EventSerializer(I18nAwareModelSerializer):
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
@@ -238,10 +249,11 @@ class EventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Item Meta properties
if item_meta_properties is not None:
@@ -279,23 +291,25 @@ class EventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
current = [imp for imp in event.item_meta_properties.all()]
current = list(event.item_meta_properties.all())
for key, value in item_meta_properties.items():
prop = self.item_meta_props.get(key)
if prop in current:
@@ -395,8 +409,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
'seat_category_mapping', 'last_modified')
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'meta_data', 'seat_category_mapping', 'last_modified')
def validate(self, data):
data = super().validate(data)
@@ -444,11 +458,22 @@ class SubEventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
@@ -465,10 +490,11 @@ class SubEventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Seats
if subevent.seating_plan:
@@ -514,19 +540,21 @@ class SubEventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
@@ -568,6 +596,7 @@ class EventSettingsSerializer(SettingsSerializer):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
@@ -580,10 +609,16 @@ class EventSettingsSerializer(SettingsSerializer):
'locale',
'region',
'last_order_modification_date',
'allow_modifications_after_checkin',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -594,6 +629,7 @@ class EventSettingsSerializer(SettingsSerializer):
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'event_info_text',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
@@ -627,6 +663,7 @@ class EventSettingsSerializer(SettingsSerializer):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',

View File

@@ -18,18 +18,18 @@ class FormFieldWrapperField(serializers.Field):
simple_mappings = (
(forms.DateField, serializers.DateField, tuple()),
(forms.TimeField, serializers.TimeField, tuple()),
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()),
(forms.DateTimeField, serializers.DateTimeField, tuple()),
(forms.DateField, serializers.DateField, ()),
(forms.TimeField, serializers.TimeField, ()),
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
(forms.DateTimeField, serializers.DateTimeField, ()),
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
(forms.FloatField, serializers.FloatField, tuple()),
(forms.IntegerField, serializers.IntegerField, tuple()),
(forms.EmailField, serializers.EmailField, tuple()),
(forms.UUIDField, serializers.UUIDField, tuple()),
(forms.URLField, serializers.URLField, tuple()),
(forms.NullBooleanField, serializers.NullBooleanField, tuple()),
(forms.BooleanField, serializers.BooleanField, tuple()),
(forms.FloatField, serializers.FloatField, ()),
(forms.IntegerField, serializers.IntegerField, ()),
(forms.EmailField, serializers.EmailField, ()),
(forms.UUIDField, serializers.UUIDField, ()),
(forms.URLField, serializers.URLField, ()),
(forms.NullBooleanField, serializers.NullBooleanField, ()),
(forms.BooleanField, serializers.BooleanField, ()),
)

View File

@@ -25,7 +25,7 @@ from pretix.base.models import (
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
)
from pretix.base.pdf import get_variables
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.pricing import get_price
@@ -45,6 +45,14 @@ class CompatibleCountryField(serializers.Field):
return instance.country_old
class CountryField(serializers.Field):
def to_internal_value(self, data):
return {self.field_name: Country(data)}
def to_representation(self, src):
return str(src) if src else None
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
@@ -112,6 +120,17 @@ class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
def to_representation(self, instance):
r = super().to_representation(instance)
if r['answer'].startswith('file://') and instance.orderposition:
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
'organizer': instance.orderposition.order.event.organizer.slug,
'event': instance.orderposition.order.event.slug,
'pk': instance.orderposition.pk,
'question': instance.question_id,
}, request=self.context['request'])
return r
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
@@ -265,6 +284,9 @@ class PdfDataSerializer(serializers.Field):
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
if 'vars_images' not in self.context:
self.context['vars_images'] = get_images(self.context['request'].event)
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
@@ -279,7 +301,28 @@ class PdfDataSerializer(serializers.Field):
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
return res
res['images'] = {}
for k, f in self.context['vars_images'].items():
if 'etag' in f:
has_image = etag = f['etag'](instance, instance.order, ev)
else:
has_image = f['etag'](instance, instance.order, ev)
etag = None
if has_image:
url = reverse('api-v1:orderposition-pdf_image', kwargs={
'organizer': instance.order.event.organizer.slug,
'event': instance.order.event.slug,
'pk': instance.pk,
'key': k,
}, request=self.context['request'])
if etag:
url += f'#etag={etag}'
res['images'][k] = url
else:
res['images'][k] = None
return res
class OrderPositionSerializer(I18nAwareModelSerializer):
@@ -1287,17 +1330,24 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
class InvoiceSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True)
invoice_to_country = CountryField()
invoice_from_country = CountryField()
class Meta:
model = Invoice
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference')

View File

@@ -213,6 +213,8 @@ class TeamMemberSerializer(serializers.ModelSerializer):
class OrganizerSettingsSerializer(SettingsSerializer):
default_fields = [
'contact_mail',
'imprint_url',
'organizer_info_text',
'event_list_type',
'event_list_availability',

View File

@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
read_only_fields = ('id', 'created', 'voucher')
def validate(self, data):
@@ -32,4 +32,11 @@ class WaitingListSerializer(I18nAwareModelSerializer):
if availability[0] == 100:
raise ValidationError("This product is currently available.")
if data.get('name') and data.get('name_parts'):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = event.settings.name_scheme
return data

View File

@@ -53,8 +53,8 @@ class DeviceSerializer(serializers.ModelSerializer):
class InitializeView(APIView):
authentication_classes = tuple()
permission_classes = tuple()
authentication_classes = ()
permission_classes = ()
def post(self, request, format=None):
serializer = InitializationRequestSerializer(data=request.data)

View File

@@ -1,4 +1,6 @@
import datetime
import mimetypes
import os
from decimal import Decimal
import django_filters
@@ -12,6 +14,7 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from PIL import Image
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
@@ -35,8 +38,9 @@ from pretix.base.models import (
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
TaxRule, TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import RevokedTicketSecret
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
from pretix.base.services.invoices import (
@@ -913,6 +917,62 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
'tax_rule': tr.pk if tr else None,
})
@action(detail=True, url_name='answer', url_path=r'answer/(?P<question>\d+)')
def answer(self, request, **kwargs):
pos = self.get_object()
answer = get_object_or_404(
QuestionAnswer,
orderposition=self.get_object(),
question_id=kwargs.get('question')
)
if not answer.file:
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
)
return resp
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
def pdf_image(self, request, key, **kwargs):
pos = self.get_object()
image_vars = get_images(request.event)
if key not in image_vars:
raise NotFound('Unknown key')
image_file = image_vars[key]['evaluate'](pos, pos.order, pos.subevent or self.request.event)
if image_file is None:
raise NotFound('No image available')
if getattr(image_file, 'name', ''):
ftype, ignored = mimetypes.guess_type(image_file.name)
extension = os.path.basename(image_file.name).split('.')[-1]
else:
img = Image.open(image_file)
ftype = Image.MIME[img.format]
extensions = {
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
}
extension = extensions.get(img.format, 'bin')
if hasattr(image_file, 'seek'):
image_file.seek(0)
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)

View File

@@ -2,10 +2,12 @@ import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.timezone import now
@@ -128,9 +130,21 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if order:
htmlctx['order'] = order
positions = list(order.positions.select_related(
'item', 'variation', 'subevent', 'addon_to'
).annotate(
has_addons=Count('addons')
))
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
positions, key=lambda op: (
op.item, op.variation, op.subevent, op.attendee_name,
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
)
)]
if position:
htmlctx['position'] = position
htmlctx['ev'] = position.subevent or self.event
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
@@ -237,6 +251,8 @@ def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'position' in kwargs:
kwargs.setdefault("position_or_address", kwargs['position'])
if 'order' in kwargs:
try:
kwargs['invoice_address'] = kwargs['order'].invoice_address
@@ -468,7 +484,8 @@ def base_placeholders(sender, **kwargs):
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(

View File

@@ -1,4 +1,5 @@
import io
import re
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
@@ -10,11 +11,21 @@ from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
from pretix.base.models import Event
def excel_safe(val):
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
class BaseExporter:
"""
This is the base class for all data exporters
@@ -181,7 +192,7 @@ class ListExporter(BaseExporter):
total = line.total
continue
ws.append([
str(val) if not isinstance(val, KNOWN_TYPES) else val
excel_safe(val) if not isinstance(val, KNOWN_TYPES) else val
for val in line
])
if total:
@@ -242,7 +253,10 @@ class MultiSheetListExporter(ListExporter):
pass
def iterate_sheet(self, form_data, sheet):
raise NotImplementedError() # noqa
if hasattr(self, 'iterate_' + sheet):
yield from getattr(self, 'iterate_' + sheet)(form_data)
else:
raise NotImplementedError() # noqa
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
total = 0
@@ -288,6 +302,9 @@ class MultiSheetListExporter(ListExporter):
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
if hasattr(self, 'prepare_xlsx_sheet_' + s):
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
total = 0
counter = 0
for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
@@ -295,7 +312,7 @@ class MultiSheetListExporter(ListExporter):
total = line.total
continue
ws.append([
str(val) if not isinstance(val, KNOWN_TYPES) else val
excel_safe(val)
for val in line
])
if total:

View File

@@ -18,6 +18,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter
from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
@@ -66,7 +67,7 @@ class InvoiceExporterMixin:
)
def invoices_queryset(self, form_data: dict):
qs = Invoice.objects.filter(event__in=self.events)
qs = Invoice.objects.filter(event__in=self.events).select_related('order')
if form_data.get('payment_provider'):
qs = qs.annotate(
@@ -111,14 +112,16 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
if not i.file:
raise ExportError('Could not generate PDF for invoice {nr}'.format(nr=i.full_invoice_no))
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
i.file.close()
counter += 1
if total and counter % max(10, total // 100) == 0:

View File

@@ -691,13 +691,18 @@ class QuotaListExporter(ListExporter):
verbose_name = gettext_lazy('Quota availabilities')
def iterate_list(self, form_data):
has_subevents = self.event.has_subevents
headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
]
if has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
yield headers
quotas = list(self.event.quotas.all())
quotas = list(self.event.quotas.select_related('subevent'))
qa = QuotaAvailability(full_results=True)
qa.queue(*quotas)
qa.compute()
@@ -715,6 +720,18 @@ class QuotaListExporter(ListExporter):
qa.count_exited_orders[quota],
_('Infinite') if avail[1] is None else avail[1]
]
if has_subevents:
if quota.subevent:
row.append(quota.subevent.name)
row.append(quota.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if quota.subevent.date_to:
row.append(quota.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
else:
row.append('')
row.append('')
row.append('')
yield row
def get_filename(self):

View File

@@ -82,7 +82,9 @@ class WaitingListExporter(ListExporter):
headers = [
_('Date'),
_('Name'),
_('Email'),
_('Phone number'),
_('Product name'),
_('Variation'),
_('Event slug'),
@@ -117,7 +119,9 @@ class WaitingListExporter(ListExporter):
row = [
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
entry.name,
entry.email,
entry.phone,
str(entry.item) if entry.item else "",
str(entry.variation) if entry.variation else "",
entry.event.slug,

View File

@@ -100,7 +100,7 @@ class NamePartsWidget(forms.MultiWidget):
if not isinstance(value, list):
value = self.decompress(value)
output = []
final_attrs = self.build_attrs(attrs or dict())
final_attrs = self.build_attrs(attrs or {})
if 'required' in final_attrs:
del final_attrs['required']
id_ = final_attrs.get('id', None)
@@ -122,6 +122,8 @@ class NamePartsWidget(forms.MultiWidget):
these_attrs.pop('data-no-required-attr', None)
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
these_attrs['data-size'] = self.scheme['fields'][i][2]
if len(self.widgets) > 1:
these_attrs['aria-label'] = self.scheme['fields'][i][1]
else:
these_attrs = final_attrs
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
@@ -220,7 +222,7 @@ class WrappedPhonePrefixSelect(Select):
country_name = locale.territories.get(country_code)
if country_name:
choices.append((prefix, "{} {}".format(country_name, prefix)))
super().__init__(choices=sorted(choices, key=lambda item: item[1]))
super().__init__(choices=sorted(choices, key=lambda item: item[1]), attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
def render(self, name, value, *args, **kwargs):
return super().render(name, value or self.initial, *args, **kwargs)
@@ -243,7 +245,10 @@ class WrappedPhonePrefixSelect(Select):
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def __init__(self, attrs=None, initial=None):
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput())
attrs = {
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)')
}
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs=attrs))
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
def render(self, name, value, attrs=None, renderer=None):
@@ -444,6 +449,7 @@ class BaseQuestionsForm(forms.Form):
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
cc = None
state = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif country:
@@ -452,6 +458,7 @@ class BaseQuestionsForm(forms.Form):
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
state = (cartpos.state if cartpos else orderpos.state)
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
@@ -460,6 +467,7 @@ class BaseQuestionsForm(forms.Form):
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
initial=state,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),
@@ -848,11 +856,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if data.get('vat_id') and is_eu_country(data.get('country')) and data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'):
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
try:
result = vat_moss.id.validate(data.get('vat_id'))
if result:

View File

@@ -16,7 +16,7 @@ class DatePickerWidget(forms.DateInput):
date_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control')
date_attrs['class'] += ' datepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
date_attrs['autocomplete'] = 'off'
def placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
@@ -37,7 +37,7 @@ class TimePickerWidget(forms.TimeInput):
time_attrs = dict(attrs)
time_attrs.setdefault('class', 'form-control')
time_attrs['class'] += ' timepickerfield'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
time_attrs['autocomplete'] = 'off'
def placeholder():
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
@@ -111,8 +111,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
date_attrs['autocomplete'] = 'off'
time_attrs['autocomplete'] = 'off'
if min_date:
date_attrs['data-min'] = (
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()

View File

@@ -12,12 +12,10 @@ from django.utils.formats import date_format, localize
from django.utils.translation import (
get_language, gettext, gettext_lazy, pgettext,
)
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
@@ -31,6 +29,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Invoice, Order
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader
logger = logging.getLogger(__name__)
@@ -221,26 +220,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
class ThumbnailingImageReader(ImageReader):
def resize(self, width, height, dpi):
if width is None:
width = height * self._image.size[0] / self._image.size[1]
if height is None:
height = width * self._image.size[1] / self._image.size[0]
self._image.thumbnail(
size=(int(width * dpi / 72), int(height * dpi / 72)),
resample=BICUBIC
)
self._data = None
return width, height
def _jpeg_fh(self):
# Bypass a reportlab-internal optimization that falls back to the original
# file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image.
return None
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
@@ -276,8 +255,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
'InvoiceFrom'])
p = Paragraph(
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
@@ -382,6 +363,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
@@ -462,13 +444,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = []
if self.invoice.custom_field:
story.append(Paragraph(
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
'{}: {}'.format(
bleach.clean(str(self.invoice.event.settings.invoice_address_custom_field), tags=[]).strip().replace('\n', '<br />\n'),
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
pgettext('invoice', 'Customer reference: {reference}').format(
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
),
self.stylesheet['Normal']
))
@@ -487,7 +474,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
if self.invoice.introductory_text:
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Paragraph(
self.invoice.introductory_text,
self.stylesheet['Normal']
))
story.append(Spacer(1, 10 * mm))
return story
@@ -587,10 +577,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 15 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
story.append(Paragraph(
self.invoice.payment_provider_text,
self.stylesheet['Normal']
))
if self.invoice.additional_text:
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
story.append(Paragraph(
self.invoice.additional_text,
self.stylesheet['Normal']
))
story.append(Spacer(1, 15 * mm))
tstyledata = [
@@ -722,7 +718,10 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from:
return
c = self.invoice.address_invoice_from.strip().split('\n')
c = [
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)

View File

@@ -16,6 +16,8 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
'(dotted path, comma separation)')
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
'(dotted path, comma separation)')
def handle(self, *args, **options):
verbosity = int(options['verbosity'])
@@ -28,9 +30,12 @@ class Command(BaseCommand):
if options.get('tasks'):
if name not in options.get('tasks').split(','):
continue
if options.get('exclude'):
if name in options.get('exclude').split(','):
continue
if verbosity > 1:
self.stdout.write(f'Running {name}')
self.stdout.write(f'INFO Running {name}')
t0 = time.time()
try:
r = receiver(signal=periodic_task, sender=self)
@@ -40,13 +45,13 @@ class Command(BaseCommand):
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception
capture_exception(err)
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
else:
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
traceback.print_exc()
else:
if options.get('verbosity') > 1:
if r is SKIPPED:
self.stdout.write(self.style.SUCCESS(f'Skipped {name}'))
self.stdout.write(self.style.SUCCESS(f'INFO Skipped {name}'))
else:
self.stdout.write(self.style.SUCCESS(f'Completed {name} in {round(time.time() - t0, 3)}s'))
self.stdout.write(self.style.SUCCESS(f'INFO Completed {name} in {round(time.time() - t0, 3)}s'))

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.0.11 on 2021-02-05 15:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0175_orderrefund_comment'),
]
operations = [
migrations.AddField(
model_name='eventmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='eventmetaproperty',
name='protected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='eventmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.0.10 on 2021-03-01 15:10
import jsonfallback.fields
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0176_auto_20210205_1512'),
]
operations = [
migrations.AddField(
model_name='waitinglistentry',
name='name_cached',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='waitinglistentry',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AddField(
model_name='waitinglistentry',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.0.12 on 2021-03-08 13:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0177_auto_20210301_1510'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='attendee_name',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='event_date_to',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='item',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item'),
),
migrations.AddField(
model_name='invoiceline',
name='variation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation'),
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 3.0.10 on 2021-03-11 16:53
from django.db import migrations
def clean_duplicates(apps, schema_editor):
while True:
delete_options = """
DELETE
FROM pretixbase_questionanswer_options
WHERE questionanswer_id IN (
SELECT MIN(qa.id)
FROM pretixbase_questionanswer qa
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
HAVING COUNT(*) > 1
);
"""
delete_answers = """
DELETE
FROM pretixbase_questionanswer
WHERE pretixbase_questionanswer.id IN (
SELECT MIN(qa.id)
FROM pretixbase_questionanswer qa
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
HAVING COUNT(*) > 1
);
"""
with schema_editor.connection.cursor() as cursor:
cursor.execute(delete_options)
cursor.execute(delete_answers)
if cursor.rowcount == 0:
return
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0178_auto_20210308_1326'),
]
operations = [
migrations.RunPython(
clean_duplicates,
migrations.RunPython.noop,
),
migrations.AlterUniqueTogether(
name='questionanswer',
unique_together={('orderposition', 'question'), ('cartposition', 'question')},
),
]

View File

@@ -16,11 +16,12 @@ from django.core.validators import (
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
from django.template.defaultfilters import date as _date
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
@@ -696,9 +697,9 @@ class Event(EventMixin, LoggedModel):
for k, v in rules.items():
if k == 'lookup':
if v[0] == 'product':
v[1] = str(item_map.get(int(v[1]), 0).pk)
v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
elif v[0] == 'variation':
v[1] = str(variation_map.get(int(v[1]), 0).pk)
v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
else:
_walk_rules(v)
elif isinstance(rules, list):
@@ -861,7 +862,7 @@ class Event(EventMixin, LoggedModel):
Returns the currently configured ticket secret generator.
"""
tsgs = self.ticket_secret_generators
return tsgs[self.settings.ticket_secret_generator]
return tsgs.get(self.settings.ticket_secret_generator, tsgs.get('random'))
def get_data_shredders(self) -> dict:
"""
@@ -953,6 +954,18 @@ class Event(EventMixin, LoggedModel):
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
for mp in self.organizer.meta_properties.all():
if mp.required and not self.meta_data.get(mp.name):
issues.append(
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
property=mp.name,
a_attr='href="%s#id_prop-%d-value"' % (
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
mp.pk
)
)
)
responses = event_live_issues.send(self)
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
if response:
@@ -1363,7 +1376,26 @@ class EventMetaProperty(LoggedModel):
],
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
default = models.TextField(blank=True, verbose_name=_("Default value"))
protected = models.BooleanField(default=False,
verbose_name=_("Can only be changed by organizer-level administrators"))
required = models.BooleanField(
default=False, verbose_name=_("Required for events"),
help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always "
"optional to set a value for individual dates")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
def full_clean(self, exclude=None, validate_unique=True):
super().full_clean(exclude, validate_unique)
if self.default and self.required:
raise ValidationError(_("A property can either be required or have a default value, not both."))
if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines():
raise ValidationError(_("You cannot set a default value that is not a valid value."))
class EventMetaValue(LoggedModel):

View File

@@ -273,6 +273,14 @@ class InvoiceLine(models.Model):
:type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created
:type event_date_from: datetime
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
:type event_date_to: datetime
:param item: The item this line refers to
:type item: Item
:param variation: The variation this line refers to
:type variation: ItemVariation
:param attendee_name: The attendee name at the time the invoice was created
:type attendee_name: str
"""
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0)
@@ -283,6 +291,10 @@ class InvoiceLine(models.Model):
tax_name = models.CharField(max_length=190)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=True)
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
attendee_name = models.TextField(null=True, blank=True)
@property
def net_value(self):

View File

@@ -2,7 +2,6 @@ import copy
import hashlib
import json
import logging
import os
import string
from collections import Counter
from datetime import datetime, time, timedelta
@@ -666,6 +665,8 @@ class Order(LockModel, LoggedModel):
related to the order. This checks order status and modification deadlines. It also
returns ``False`` if there are no questions that can be answered.
"""
from .checkin import Checkin
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
@@ -681,10 +682,21 @@ class Order(LockModel, LoggedModel):
if modify_deadline is not None and now() > modify_deadline:
return False
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item').prefetch_related('item__questions')
)
if not self.event.settings.allow_modifications_after_checkin:
for cp in positions:
if cp.has_checkin:
return False
if self.event.settings.get('invoice_address_asked', as_type=bool):
return True
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in self.positions.all().prefetch_related('item__questions'):
for cp in positions:
if (cp.item.admission and ask_names) or cp.item.questions.all():
return True
@@ -971,6 +983,9 @@ class QuestionAnswer(models.Model):
objects = ScopedManager(organizer='question__event__organizer')
class Meta:
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
@property
def backend_file_url(self):
if self.file:
@@ -1492,7 +1507,7 @@ class OrderPayment(models.Model):
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
if payment_sum - refund_sum < self.order.total:
logger.info('Confirmed payment {} but payment sum is {} and refund sum is.'.format(
logger.info('Confirmed payment {} but payment sum is {} and refund sum is {}.'.format(
self.full_id, payment_sum, refund_sum
))
return
@@ -2320,7 +2335,6 @@ def cachedticket_name(instance, filename: str) -> str:
no=instance.order_position.positionid,
code=instance.order_position.order.code,
secret=secret,
ext=os.path.splitext(filename)[1]
)

View File

@@ -4,6 +4,7 @@ from datetime import date, datetime, time
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
@@ -88,6 +89,15 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
@cached_property
def all_logentries_link(self):
return reverse(
'control:organizer.log',
kwargs={
'organizer': self.slug,
}
)
@property
def has_gift_cards(self):
return self.cache.get_or_set(

View File

@@ -5,11 +5,14 @@ from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from jsonfallback.fields import FallbackJSONField
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LoggedModel
from .event import Event, SubEvent
@@ -37,9 +40,21 @@ class WaitingListEntry(LoggedModel):
verbose_name=_("On waiting list since"),
auto_now_add=True
)
name_cached = models.CharField(
max_length=255,
verbose_name=_("Name"),
blank=True, null=True,
)
name_parts = FallbackJSONField(
blank=True, default=dict
)
email = models.EmailField(
verbose_name=_("E-mail address")
)
phone = PhoneNumberField(
null=True, blank=True,
verbose_name=_("Phone number")
)
voucher = models.ForeignKey(
'Voucher',
verbose_name=_("Assigned voucher"),
@@ -83,6 +98,27 @@ class WaitingListEntry(LoggedModel):
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
if 'name_parts' in update_fields:
update_fields.append('name_cached')
self.name_cached = self.name
if self.name_parts is None:
self.name_parts = {}
super().save(*args, **kwargs)
@property
def name(self):
if not self.name_parts:
return None
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.name_parts).strip()
def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)

View File

@@ -718,7 +718,7 @@ class BasePaymentProvider:
return a very short version of the payment method. Usually, this should return e.g.
a transaction ID or account identifier, but no information on status, dates, etc.
The default implementation falls back to payment_presa_elrender.
The default implementation falls back to ``payment_presale_render``.
:param order: The order object
"""
@@ -970,17 +970,17 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
('pending_description', I18nFormField(
label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
] + list(super().settings_form_fields.items())
)
@@ -1001,21 +1001,24 @@ class ManualPayment(BasePaymentProvider):
def checkout_confirm_render(self, request):
return self.payment_form_render(request)
def format_map(self, order):
def format_map(self, order, payment):
return {
'order': order.code,
'total': order.total,
'amount': payment.amount,
'currency': self.event.currency,
'total_with_currency': money_filter(order.total, self.event.currency)
'amount_with_currency': money_filter(payment.amount, self.event.currency),
# {total} and {total_with_currency} are deprecated
'total': order.total,
'total_with_currency': money_filter(order.total, self.event.currency),
}
def order_pending_mail_render(self, order) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
def order_pending_mail_render(self, order, payment) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
return msg
def payment_pending_render(self, request, payment) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order))
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order, payment))
)

View File

@@ -1,4 +1,5 @@
import copy
import hashlib
import itertools
import logging
import os
@@ -35,9 +36,9 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.presale.style import get_fonts
@@ -154,6 +155,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(ev.name)
}),
("event_series_name", {
"label": _("Event series"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(order.event.name)
}),
("event_date", {
"label": _("Event date"),
"editor_sample": _("May 31st, 2017"),
@@ -292,6 +298,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
("event_info_text", {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", {
"label": _("Printing date"),
"editor_sample": _("2017-05-31"),
@@ -339,6 +350,47 @@ DEFAULT_VARIABLES = OrderedDict((
"evaluate": lambda op, order, ev: str(op.seat.seat_number if op.seat else "")
}),
))
DEFAULT_IMAGES = OrderedDict([])
@receiver(layout_image_variables, dispatch_uid="pretix_base_layout_image_variables_questions")
def images_from_questions(sender, *args, **kwargs):
def get_answer(op, order, event, question_id, etag):
a = None
if op.addon_to:
if 'answers' in getattr(op.addon_to, '_prefetched_objects_cache', {}):
try:
a = [a for a in op.addon_to.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.addon_to.answers.filter(question_id=question_id).first()
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
try:
a = [a for a in op.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
return None
else:
if etag:
return hashlib.sha1(a.file.name.encode()).hexdigest()
return a.file
d = {}
for q in sender.questions.all():
if q.type != Question.TYPE_FILE:
continue
d['question_{}'.format(q.identifier)] = {
'label': _('Question: {question}').format(question=q.question),
'evaluate': partial(get_answer, question_id=q.pk, etag=False),
'etag': partial(get_answer, question_id=q.pk, etag=True),
}
return d
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
@@ -369,6 +421,8 @@ def variables_from_questions(sender, *args, **kwargs):
d = {}
for q in sender.questions.all():
if q.type == Question.TYPE_FILE:
continue
d['question_{}'.format(q.pk)] = {
'label': _('Question: {question}').format(question=q.question),
'editor_sample': _('<Answer: {question}>').format(question=q.question),
@@ -387,6 +441,15 @@ def _get_ia_name_part(key, op, order, ev):
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
def get_images(event):
v = copy.copy(DEFAULT_IMAGES)
for recv, res in layout_image_variables.send(sender=event):
v.update(res)
return v
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
@@ -427,6 +490,7 @@ class Renderer:
self.layout = layout
self.background_file = background_file
self.variables = get_variables(event)
self.images = get_images(event)
self.event = event
if self.background_file:
self.bg_bytes = self.background_file.read()
@@ -514,6 +578,47 @@ class Renderer:
return '(error)'
return ''
def _draw_imagearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
ev = self._get_ev(op, order)
if not o['content'] or o['content'] not in self.images:
image_file = None
else:
try:
image_file = self.images[o['content']]['evaluate'](op, order, ev)
except:
logger.exception('Failed to process variable.')
image_file = None
if image_file:
ir = ThumbnailingImageReader(image_file)
try:
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(
image=ir,
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
preserveAspectRatio=True,
anchor='c', # centered in frame
mask='auto'
)
else:
canvas.saveState()
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
canvas.rect(
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
stroke=0,
fill=1,
)
canvas.restoreState()
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
font = o['fontfamily']
if o['bold']:
@@ -572,6 +677,8 @@ class Renderer:
for o in self.layout:
if o['type'] == "barcodearea":
self._draw_barcodearea(canvas, op, o)
elif o['type'] == "imagearea":
self._draw_imagearea(canvas, op, order, o)
elif o['type'] == "textarea":
self._draw_textarea(canvas, op, order, o)
elif o['type'] == "poweredby":

View File

@@ -4,7 +4,7 @@ import struct
from cryptography.hazmat.backends.openssl.backend import Backend
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization.base import (
from cryptography.hazmat.primitives.serialization import (
Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
load_pem_public_key,
)

View File

@@ -241,7 +241,7 @@ class CartManager:
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
@@ -863,7 +863,7 @@ class CartManager:
op.position.addons.all().delete()
op.position.delete()
elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
# Create a CartPosition for as much items as we can
requested_count = quota_available_count = voucher_available_count = op.count

View File

@@ -102,9 +102,11 @@ class RequiredQuestionsError(Exception):
def _save_answers(op, answers, given_answers):
written = False
for q, a in given_answers.items():
if not a:
if q in answers:
written = True
answers[q].delete()
else:
continue
@@ -113,6 +115,7 @@ def _save_answers(op, answers, given_answers):
qa = answers[q]
qa.answer = str(a.answer)
qa.save()
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=str(a.answer))
@@ -122,6 +125,7 @@ def _save_answers(op, answers, given_answers):
qa = answers[q]
qa.answer = ", ".join([str(o) for o in a])
qa.save()
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
@@ -134,6 +138,7 @@ def _save_answers(op, answers, given_answers):
qa.file.save(a.name, a, save=False)
qa.answer = 'file://' + qa.file.name
qa.save()
written = True
else:
if q in answers:
qa = answers[q]
@@ -141,9 +146,14 @@ def _save_answers(op, answers, given_answers):
qa.save()
else:
op.answers.create(question=q, answer=str(a))
written = True
if written:
prefetched_objects_cache = getattr(op, '_prefetched_objects_cache', {})
if 'answers' in prefetched_objects_cache:
del prefetched_objects_cache['answers']
@transaction.atomic
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):
@@ -163,18 +173,16 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
"""
dt = datetime or now()
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
checkin_questions = list(
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
)
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'
)
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
checkin_questions = list(
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
)
require_answers = []
if checkin_questions:
answers = {a.question: a for a in op.answers.all()}
@@ -184,81 +192,85 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
_save_answers(op, answers, given_answers)
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 not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
raise CheckInError(
_('This order is not marked as paid.'),
'unpaid'
)
elif require_answers and not force and questions_supported:
raise RequiredQuestionsError(
_('You need to answer questions to complete this check-in.'),
'incomplete',
require_answers
)
with transaction.atomic():
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
rule_data = LazyRuleVars(op, clist, dt)
logic = get_logic_environment(op.subevent or clist.event)
if not logic.apply(clist.rules, rule_data):
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
raise CheckInError(
_('This entry is not permitted due to custom rules.'),
'rules'
_('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 not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
raise CheckInError(
_('This order is not marked as paid.'),
'unpaid'
)
elif require_answers and not force and questions_supported:
raise RequiredQuestionsError(
_('You need to answer questions to complete this check-in.'),
'incomplete',
require_answers
)
device = None
if isinstance(auth, Device):
device = auth
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
rule_data = LazyRuleVars(op, clist, dt)
logic = get_logic_environment(op.subevent or clist.event)
if not logic.apply(clist.rules, rule_data):
raise CheckInError(
_('This entry is not permitted due to custom rules.'),
'rules'
)
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
)
device = None
if isinstance(auth, Device):
device = auth
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and not entry_allowed,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'datetime': dt,
'type': type,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
raise CheckInError(
_('This ticket has already been redeemed.'),
'already_redeemed',
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
)
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and not entry_allowed,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'datetime': dt,
'type': type,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
raise CheckInError(
_('This ticket has already been redeemed.'),
'already_redeemed',
)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
def order_placed(sender, **kwargs):

View File

@@ -171,9 +171,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create(
position=i, invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
position=i,
invoice=invoice,
description=desc,
gross_value=p.price,
tax_value=p.tax_value,
subevent=p.subevent,
item=p.item,
variation=p.variation,
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)
@@ -198,6 +206,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice=invoice,
description=fee_title,
gross_value=fee.value,
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
tax_value=fee.tax_value,
tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else ''

View File

@@ -19,6 +19,7 @@ from django.core.mail import (
EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
)
from django.core.mail.message import SafeMIMEText
from django.db import transaction
from django.template.loader import get_template
from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled
@@ -240,7 +241,15 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
task_chain = []
task_chain.append(send_task)
chain(*task_chain).apply_async()
if 'locmem' in settings.EMAIL_BACKEND:
# This clause is triggered during unit tests, because transaction.on_commit never fires due to the nature
# Django's unit tests work
chain(*task_chain).apply_async()
else:
transaction.on_commit(
lambda: chain(*task_chain).apply_async()
)
class CustomEmail(EmailMultiAlternatives):
@@ -291,6 +300,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
order = None
else:
with language(order.locale, event.settings.region):
if not event.settings.mail_attach_tickets:
attach_tickets = False
if position:
try:
position = order.positions.get(pk=position)

View File

@@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None, keep_fees=None):
cancellation_fee=None, keep_fees=None, cancel_invoice=True):
"""
Mark this order as canceled
:param order: The order to change
@@ -351,9 +351,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
invoices = []
i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists():
invoices.append(generate_cancellation(i))
if cancel_invoice:
i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists():
invoices.append(generate_cancellation(i))
for position in order.positions.all():
for gc in position.issued_gift_cards.all():
@@ -403,7 +404,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
if i:
if cancel_invoice and i:
invoices.append(generate_invoice(order))
else:
with order.event.lock():
@@ -2152,11 +2153,12 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None):
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None,
cancel_invoice=True):
try:
try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
cancellation_fee, cancel_invoice=cancel_invoice)
if try_auto_refund:
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
comment=comment)

View File

@@ -3,22 +3,21 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Event, User, Voucher
from pretix.base.models import Event, LogEntry, User, Voucher
from pretix.base.services.mail import mail
from pretix.base.services.tasks import TransactionAwareProfiledEventTask
from pretix.celery_app import app
@app.task(base=TransactionAwareProfiledEventTask, acks_late=True)
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int) -> None:
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int,
progress=None) -> None:
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
user = User.objects.get(pk=user)
for r in recipients:
for ir, r in enumerate(recipients):
voucher_list = []
for i in range(r['number']):
voucher_list.append(vouchers.pop())
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '', voucher_list=[v.code for v in voucher_list])
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
mail(
r['email'],
subject,
@@ -27,14 +26,14 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
event,
locale=event.settings.locale,
)
logs = []
for v in voucher_list:
if r.get('tag') and r.get('tag') != v.tag:
v.tag = r.get('tag')
if v.comment:
v.comment += '\n\n'
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
v.save(update_fields=['tag', 'comment'])
v.log_action(
logs.append(v.log_action(
'pretix.voucher.sent',
user=user,
data={
@@ -42,5 +41,11 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
'name': r.get('name'),
'subject': subject,
'message': message,
}
)
},
save=False
))
Voucher.objects.bulk_update(voucher_list, fields=['comment', 'tag'], batch_size=500)
LogEntry.objects.bulk_create(logs, batch_size=500)
if progress and ir % 50 == 0:
progress(ir / len(recipients))

View File

@@ -13,6 +13,7 @@ from django.core.validators import (
MaxValueValidator, MinValueValidator, RegexValidator,
)
from django.db.models import Model
from django.utils.text import format_lazy
from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
)
@@ -974,6 +975,61 @@ DEFAULTS = {
widget=forms.NumberInput(),
)
},
'waiting_list_names_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a name"),
help_text=_("Ask for a name when signing up to the waiting list."),
)
},
'waiting_list_names_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require name"),
help_text=_("Require a name when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_names_asked'}),
)
},
'waiting_list_phones_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a phone number"),
help_text=_("Ask for a phone number when signing up to the waiting list."),
)
},
'waiting_list_phones_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require phone number"),
help_text=_("Require a phone number when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_phones_asked'}),
)
},
'waiting_list_phones_explanation_text': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Phone number explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
},
'ticket_download': {
'default': 'False',
'type': bool,
@@ -1082,6 +1138,15 @@ DEFAULTS = {
help_text=_('If your event series has more than 50 dates in the future, only the month or week calendar can be used.')
),
},
'allow_modifications_after_checkin': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Allow customers to modify their information after they checked in."),
)
},
'last_order_modification_date': {
'default': None,
'type': RelativeDateWrapper,
@@ -1313,6 +1378,19 @@ DEFAULTS = {
'default': 'classic',
'type': str
},
'mail_attach_tickets': {
'default': 'True',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Attach ticket files"),
help_text=format_lazy(
_("Tickets will never be attached if they're larger than {size} to avoid email delivery problems."),
size='4 MB'
),
)
},
'mail_attach_ical': {
'default': 'False',
'type': bool,
@@ -1721,7 +1799,7 @@ Your {event} team"""))
),
},
'theme_color_danger': {
'default': '#D36060',
'default': '#C44F4F',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -1922,6 +2000,18 @@ Your {event} team"""))
widget=I18nTextarea
)
},
'event_info_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Info text'),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
},
'banner_text': {
'default': '',
'type': LazyI18nString,
@@ -1974,6 +2064,19 @@ Your {event} team"""))
"why you need information from them.")
)
},
'checkout_success_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Additional success message"),
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
"to the default text."),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
)
},
'checkout_phone_helptext': {
'default': '',
'type': LazyI18nString,
@@ -2367,6 +2470,22 @@ PERSON_NAME_SCHEMES = OrderedDict([
'_scheme': 'full_transcription',
},
}),
('salutation_given_family', {
'fields': (
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),
('given_name', _('Given name'), 2),
('family_name', _('Family name'), 2),
),
'concatenation': lambda d: ' '.join(
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
),
'sample': {
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
'given_name': pgettext_lazy('person_name_sample', 'John'),
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
'_scheme': 'salutation_given_family',
},
}),
('salutation_title_given_family', {
'fields': (
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),

View File

@@ -203,7 +203,7 @@ class EmailAddressShredder(BaseDataShredder):
class WaitingListShredder(BaseDataShredder):
verbose_name = _('Waiting list')
identifier = 'waiting_list'
description = _('This will remove all email addresses from the waiting list.')
description = _('This will remove all names, email addresses, and phone numbers from the waiting list.')
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'waiting-list.json', 'application/json', json.dumps([
@@ -213,7 +213,7 @@ class WaitingListShredder(BaseDataShredder):
@transaction.atomic
def shred_data(self):
self.event.waitinglistentries.update(email='')
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='', phone='')
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
if '@' in wle.voucher.comment:
@@ -222,7 +222,14 @@ class WaitingListShredder(BaseDataShredder):
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
d = le.parsed_data
if 'name' in d:
d['name'] = ''
if 'name_parts' in d:
d['name_parts'] = {
'_legacy': ''
}
d['email'] = ''
d['phone'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])

View File

@@ -613,6 +613,7 @@ If the email is associated with a specific user, e.g. a notification email, the
well, otherwise it will be ``None``.
"""
layout_text_variables = EventPluginSignal()
"""
This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts.
@@ -627,11 +628,35 @@ dictionaries as values that contain keys like in the following example::
}
}
The evaluate member will be called with the order position, order and event as arguments. The event might
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""
layout_image_variables = EventPluginSignal()
"""
This signal is sent out to collect variables that can be used to display dynamic images in ticket-related PDF layouts.
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
dictionaries as values that contain keys like in the following example::
return {
"profile": {
"label": _("Profile picture"),
"evaluate": lambda orderposition, order, event: ContentFile(b"some-image-data"),
"etag": lambda orderposition, order, event: hash(b"some-image-data")
}
}
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable. The return value of ``evaluate`` should be an instance of Django's ``File``
class and point to a valid JPEG or PNG file. If no image is available, ``evaluate`` should return ``None``.
The ``etag`` member will be called with the same arguments as ``evaluate`` but should return a ``str`` value
uniquely identifying the version of the file. This can be a hash of the file, but can also be something else.
If no image is available, ``etag`` should return ``None``. In some cases, this can speed up the implementation.
"""
timeline_events = EventPluginSignal()
"""
This signal is sent out to collect events for the time line shown on event dashboards. You are passed

View File

@@ -136,6 +136,31 @@
text-decoration: none;
color: {{ color }};
}
.order-button {
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
}
.order-info {
padding-bottom: 5px
}
.order {
font-size: 12px;
}
.cart-table > tr > td:first-child {
width: 40px;
}
.order-details > tr > td:first-child {
width: 20%;
}
.order-details td {
font-size: 12px;
}
{% if rtl %}
body {
direction: rtl;

View File

@@ -0,0 +1,142 @@
{% load eventurl %}
{% load i18n %}
{% if position %}
<div class="order-info">
{% trans "You are receiving this email because someone signed you up for the following event:" %}
</div>
<table class="order-details">
<tr>
<td>
<strong>{% trans "Event:" %}</strong>
</td>
<td>
{{ event.name }}
<br>
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}<br>{% endif %}
{{ ev.get_date_range_display }}
{% if event.settings.show_times %}
{{ ev.date_from|date:"TIME_FORMAT" }}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Order code:" %}</strong>
</td>
<td>
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})<br>
{% if order.email %}
{% trans "created by" %} {{ order.email }}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Organizer:" %}</strong>
</td>
<td>
{{ event.organizer }}
{% if event.settings.contact_mail %}
<br>
<a href="mailto:{{ event.settings.contact_mail }}">
{{ event.settings.contact_mail }}
</a>
{% endif %}
</td>
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}" class="button">
{% trans "View registration details" %}
</a>
</div>
{% else %}
<div class="order-info">
{% trans "You are receiving this email because you placed an order for the following event:" %}
</div>
<table class="order-details">
<tr>
<td>
<strong>{% trans "Event:" %}</strong>
</td>
<td>
{{ event.name }}
{% if not event.has_subevents and event.settings.show_dates_on_frontpage %}
<br>
{{ event.get_date_range_display }}
{% if event.settings.show_times %}
{{ event.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Order code:" %}</strong>
</td>
<td>
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})
</td>
</tr>
{% if cart %}
<tr>
<td>
<strong>{% trans "Details:" %}</strong>
</td>
<td>
<table class="cart-table">
{% for groupkey, positions in cart %}
<tr>
<td>
{% if not groupkey.4 %} {# is addon #}
{{ positions|length }}x
{% endif %}
</td>
<td>
{% if groupkey.4 %} {# is addon #}
+
{% endif %}
{{ groupkey.0.name }}{% if groupkey.1 %} {{ groupkey.1.value }}{% endif %}
{% if groupkey.2 %} {# subevent #}
<br>
{% if groupkey.2.name|upper != event.name|upper %}
{{ groupkey.2.name }} &middot;
{% endif %}
{{ groupkey.2.get_date_range_display }}
{% if event.settings.show_times %}
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% endif %}
{% if groupkey.3 %} {# attendee name #}
<br>
{{ groupkey.3.name }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endif %}
<tr>
<td>
<strong>{% trans "Organizer:" %}</strong>
</td>
<td>
{{ event.organizer }}
{% if event.settings.contact_mail %}
<br>
<a href="mailto:{{ event.settings.contact_mail }}">
{{ event.settings.contact_mail }}
</a>
{% endif %}
</td>
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
{% trans "View order details" %}
</a>
</div>
{% endif %}

View File

@@ -23,23 +23,7 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% if position %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
{% include "pretixbase/email/order_details.html" %}
</div>
<!--[if gte mso 9]>
</td></tr></table>

View File

@@ -147,6 +147,31 @@
text-decoration: none;
color: {{ color }};
}
.order-button {
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
}
.order-info {
padding-bottom: 5px
}
.order {
font-size: 12px;
}
.cart-table > tr > td:first-child {
width: 40px;
}
.order-details > tr > td:first-child {
width: 20%;
}
.order-details td {
font-size: 12px;
}
{% if rtl %}
body {
direction: rtl;
@@ -226,23 +251,7 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% if position %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
{% include "pretixbase/email/order_details.html" %}
</div>
<!--[if gte mso 9]>
</td></tr></table>

View File

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

View File

@@ -11,7 +11,7 @@ register = template.Library()
@register.filter("money")
def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, float) or isinstance(value, int):
if isinstance(value, (float, int)):
value = Decimal(value)
if not isinstance(value, Decimal):
if value == '':
@@ -47,7 +47,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
@register.filter("money_numberfield")
def money_numberfield_filter(value: Decimal, arg=''):
if isinstance(value, float) or isinstance(value, int):
if isinstance(value, (float, int)):
value = Decimal(value)
if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value))

View File

@@ -101,7 +101,8 @@ def truelink_callback(attrs, new=False):
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
"""
text = re.sub(r'[^a-zA-Z0-9.\-/_]', '', attrs.get('_text')) # clean up link text
href_url = urllib.parse.urlparse(attrs[None, 'href'])
url = attrs.get((None, 'href'), '/')
href_url = urllib.parse.urlparse(url)
if URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
# link text looks like a url
if text.startswith('//'):

View File

@@ -48,7 +48,7 @@ def page_not_found(request, exception):
except (AttributeError, IndexError):
pass
else:
if isinstance(message, str) or isinstance(message, Promise):
if isinstance(message, (str, Promise)):
exception_repr = str(message)
context = {
'request_path': request.path,

View File

@@ -4,43 +4,25 @@ import celery.exceptions
from celery.result import AsyncResult
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.test import RequestFactory
from django.utils.translation import gettext as _
from django.views.generic import FormView
from pretix.base.models import User
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
logger = logging.getLogger('pretix.base.tasks')
class AsyncAction:
task = None
class AsyncMixin:
success_url = None
error_url = None
known_errortypes = []
def do(self, *args, **kwargs):
if not isinstance(self.task, app.Task):
raise TypeError('Method has no task attached')
try:
res = self.task.apply_async(args=args, kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once agan
res = self.task.apply_async(args=args, kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
def get_success_url(self, value):
return self.success_url
@@ -50,11 +32,6 @@ class AsyncAction:
def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return self.http_method_not_allowed(request)
def _ajax_response_data(self):
return {}
@@ -86,7 +63,7 @@ class AsyncAction:
if smes:
messages.success(self.request, smes)
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the mssage itself
# but handle the message itself
data.update({
'redirect': self.get_success_url(res.info),
'success': True,
@@ -95,7 +72,7 @@ class AsyncAction:
else:
messages.error(self.request, self.get_error_message(res.info))
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the mssage itself
# but handle the message itself
data.update({
'redirect': self.get_error_url(),
'success': False,
@@ -159,3 +136,124 @@ class AsyncAction:
def get_success_message(self, value):
return _('The task has been completed.')
class AsyncAction(AsyncMixin):
task = None
def do(self, *args, **kwargs):
if not isinstance(self.task, app.Task):
raise TypeError('Method has no task attached')
try:
res = self.task.apply_async(args=args, kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
res = self.task.apply_async(args=args, kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return self.http_method_not_allowed(request)
class AsyncFormView(AsyncMixin, FormView):
"""
FormView variant in which instead of ``form_valid``, an ``async_form_valid``
is executed in a celery task. Note that this places some severe limitations
on the form and the view, e.g. neither ``get_form*`` nor the form itself
may depend on the request object unless specifically supported by this class.
Also, all form keyword arguments except ``instance`` need to be serializable.
"""
known_errortypes = ['ValidationError']
def __init_subclass__(cls):
def async_execute(self, request_path, form_kwargs, organizer=None, event=None, user=None):
view_instance = cls()
view_instance.request = RequestFactory().post(request_path)
if organizer:
view_instance.request.event = event
if organizer:
view_instance.request.organizer = organizer
if user:
view_instance.request.user = User.objects.get(pk=user)
form_class = view_instance.get_form_class()
if form_kwargs.get('instance'):
cls.model.objects.get(pk=form_kwargs['instance'])
form_kwargs = view_instance.get_async_form_kwargs(form_kwargs, organizer, event)
form = form_class(**form_kwargs)
return view_instance.async_form_valid(self, form)
cls.async_execute = app.task(
base=ProfiledEventTask,
bind=True,
name=cls.__module__ + '.' + cls.__name__ + '.async_execute',
throws=(ValidationError,)
)(async_execute)
def async_form_valid(self, task, form):
pass
def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
return form_kwargs
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return super().get(request, *args, **kwargs)
def form_valid(self, form):
if form.files:
raise TypeError('File upload currently not supported in AsyncFormView')
form_kwargs = {
k: v for k, v in self.get_form_kwargs().items()
}
if form_kwargs.get('instance'):
if form_kwargs['instance'].pk:
form_kwargs['instance'] = form_kwargs['instance'].pk
else:
form_kwargs['instance'] = None
form_kwargs.setdefault('data', {})
kwargs = {
'request_path': self.request.path,
'form_kwargs': form_kwargs,
}
if hasattr(self.request, 'organizer'):
kwargs['organizer'] = self.request.organizer.pk
if self.request.user.is_authenticated:
kwargs['user'] = self.request.user.pk
if hasattr(self.request, 'event'):
kwargs['event'] = self.request.event.pk
try:
res = type(self).async_execute.apply_async(kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
res = type(self).async_execute.apply_async(kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))

View File

@@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
from django.core.files import File
from django.core.files.uploadedfile import UploadedFile
from django.forms.utils import from_current_timezone
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -122,7 +123,7 @@ class CachedFileInput(forms.ClearableFileInput):
@property
def url(self):
return self.file.file.url
return reverse('cachedfile.download', kwargs={'id': self.file.id})
def value_from_datadict(self, data, files, name):
from ...base.models import CachedFile
@@ -200,6 +201,8 @@ class CachedFileField(ExtFileField):
from ...base.models import CachedFile
if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
@@ -209,6 +212,7 @@ class CachedFileField(ExtFileField):
)
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
return cf
return super().bound_data(data, initial)
@@ -217,6 +221,8 @@ class CachedFileField(ExtFileField):
data = super().clean(*args, **kwargs)
if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
web_download=True,
@@ -226,6 +232,7 @@ class CachedFileField(ExtFileField):
)
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
return cf
return data

View File

@@ -28,6 +28,13 @@ class NextTimeField(forms.TimeField):
return result
class NextTimeInput(forms.TimeInput):
def format_value(self, value):
if isinstance(value, datetime):
value = value.astimezone(get_current_timezone()).time()
return super().format_value(value)
class CheckinListForm(forms.ModelForm):
def __init__(self, **kwargs):
self.event = kwargs.pop('event')
@@ -89,7 +96,7 @@ class CheckinListForm(forms.ModelForm):
'class': 'scrolling-multiple-choice'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,

View File

@@ -265,15 +265,32 @@ class EventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
self.disabled = kwargs.pop('disabled')
super().__init__(*args, **kwargs)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
label=self.property.name,
choices=[
('', _('Default ({value})').format(value=self.property.default) if self.property.default else ''),
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
)
else:
self.fields['value'].label = self.property.name
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
self.fields['value'].required = False
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['slug']
class Meta:
model = EventMetaValue
@@ -418,6 +435,7 @@ class EventSettingsForm(SettingsForm):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
@@ -431,6 +449,11 @@ class EventSettingsForm(SettingsForm):
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -441,6 +464,7 @@ class EventSettingsForm(SettingsForm):
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'event_info_text',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
@@ -457,6 +481,7 @@ class EventSettingsForm(SettingsForm):
'banner_text_bottom',
'order_email_asked_twice',
'last_order_modification_date',
'allow_modifications_after_checkin',
'checkout_show_copy_answers_button',
'primary_color',
'theme_color_success',
@@ -768,6 +793,7 @@ class MailSettingsForm(SettingsForm):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(

View File

@@ -1,11 +1,13 @@
from datetime import datetime, time
from datetime import datetime, time, timedelta
from decimal import Decimal
from urllib.parse import urlencode
from django import forms
from django.apps import apps
from django.conf import settings
from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet
from django.db.models import (
Count, Exists, F, Max, Model, OuterRef, Q, QuerySet,
)
from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy
from django.utils.formats import date_format, localize
@@ -153,6 +155,7 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_CANCELED, _('Canceled (fully)')),
('cp', _('Canceled (fully or with paid fee)')),
('rc', _('Cancellation requested')),
('cni', _('Fully canceled but invoice not canceled')),
)),
(_('Payment process'), (
(Order.STATUS_EXPIRED, _('Expired')),
@@ -264,6 +267,18 @@ class OrderFilterForm(FilterForm):
status=Order.STATUS_PAID,
pending_sum_t__gt=0
)
elif s == 'cni':
i = Invoice.objects.filter(
order=OuterRef('pk'),
is_cancellation=False,
refered__isnull=True,
).order_by().values('order').annotate(k=Count('id')).values('k')
qs = qs.annotate(
icnt=i
).filter(
icnt__gt=0,
status=Order.STATUS_CANCELED,
)
elif s == 'pa':
qs = qs.filter(
status=Order.STATUS_PENDING,
@@ -766,10 +781,15 @@ class SubEventFilterForm(FilterForm):
),
required=False
)
date = forms.DateField(
label=_('Date'),
date_from = forms.DateField(
label=_('Date from'),
required=False,
widget=DatePickerWidget
widget=DatePickerWidget,
)
date_until = forms.DateField(
label=_('Date until'),
required=False,
widget=DatePickerWidget,
)
weekday = forms.ChoiceField(
label=_('Weekday'),
@@ -796,7 +816,8 @@ class SubEventFilterForm(FilterForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget = DatePickerWidget()
self.fields['date_from'].widget = DatePickerWidget()
self.fields['date_until'].widget = DatePickerWidget()
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -838,19 +859,21 @@ class SubEventFilterForm(FilterForm):
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
if fdata.get('date'):
date_start = make_aware(datetime.combine(
fdata.get('date'),
if fdata.get('date_until'):
date_end = make_aware(datetime.combine(
fdata.get('date_until') + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
date_end = make_aware(datetime.combine(
fdata.get('date'),
time(hour=23, minute=59, second=59, microsecond=999999)
), get_current_timezone())
qs = qs.filter(
Q(date_to__isnull=True, date_from__gte=date_start, date_from__lte=date_end) |
Q(date_to__isnull=False, date_from__lte=date_end, date_to__gte=date_start)
Q(date_to__isnull=True, date_from__lt=date_end) |
Q(date_to__isnull=False, date_to__lt=date_end)
)
if fdata.get('date_from'):
date_start = make_aware(datetime.combine(
fdata.get('date_from'),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
qs = qs.filter(date_from__gte=date_start)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
@@ -1416,6 +1439,8 @@ class VoucherFilterForm(FilterForm):
s = fdata.get('tag').strip()
if s == '<>':
qs = qs.filter(Q(tag__isnull=True) | Q(tag=''))
elif s[0] == '"' and s[-1] == '"':
qs = qs.filter(tag__iexact=s[1:-1])
else:
qs = qs.filter(tag__icontains=s)

View File

@@ -337,7 +337,7 @@ class ItemCreateForm(I18nModelForm):
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
else:
# Add to all sales channels by default
self.instance.sales_channels = [k for k in get_all_sales_channels().keys()]
self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)

View File

@@ -75,7 +75,7 @@ class ExtendForm(I18nModelForm):
return super().save(commit)
class ConfirmPaymentForm(forms.Form):
class ForceQuotaConfirmationForm(forms.Form):
force = forms.BooleanField(
label=_('Overbook quota and ignore late payment'),
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
@@ -101,10 +101,18 @@ class ConfirmPaymentForm(forms.Form):
del self.fields['force']
class CancelForm(ConfirmPaymentForm):
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
pass
class ReactivateOrderForm(ForceQuotaConfirmationForm):
pass
class CancelForm(ForceQuotaConfirmationForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify user by e-mail'),
label=_('Notify customer by email'),
initial=True
)
cancellation_fee = forms.DecimalField(
@@ -117,6 +125,11 @@ class CancelForm(ConfirmPaymentForm):
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
cancel_invoice = forms.BooleanField(
label=_('Generate cancellation for invoice'),
initial=True,
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -130,6 +143,8 @@ class CancelForm(ConfirmPaymentForm):
self.fields['cancellation_fee'].max_value = prs
else:
del self.fields['cancellation_fee']
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
@@ -139,6 +154,11 @@ class CancelForm(ConfirmPaymentForm):
class MarkPaidForm(ConfirmPaymentForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify customer by email'),
initial=True
)
amount = forms.DecimalField(
required=True,
max_digits=10, decimal_places=2,

View File

@@ -13,7 +13,9 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Device, Gate, GiftCard, Organizer, Team
from pretix.base.models import (
Device, EventMetaProperty, Gate, GiftCard, Organizer, Team,
)
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain
@@ -125,7 +127,8 @@ class OrganizerUpdateForm(OrganizerForm):
class EventMetaPropertyForm(forms.ModelForm):
class Meta:
fields = ['name', 'default']
model = EventMetaProperty
fields = ['name', 'default', 'required', 'protected', 'allowed_values']
widgets = {
'default': forms.TextInput()
}
@@ -214,6 +217,8 @@ class DeviceForm(forms.ModelForm):
class OrganizerSettingsForm(SettingsForm):
auto_fields = [
'contact_mail',
'imprint_url',
'organizer_info_text',
'event_list_type',
'event_list_availability',

View File

@@ -1,4 +1,4 @@
from bootstrap3.renderers import FieldRenderer
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value
from django.forms import CheckboxInput
from django.forms.utils import flatatt
@@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer):
optional=not required and not isinstance(self.widget, CheckboxInput)
) + html
return html
class BulkEditMixin:
def __init__(self, *args, **kwargs):
kwargs['layout'] = self.layout
super().__init__(*args, **kwargs)
def wrap_field(self, html):
field_class = self.get_field_class()
name = '{}{}'.format(self.field.form.prefix, self.field.name)
checked = self.field.form.data and name in self.field.form.data.getlist('_bulk')
html = (
'<div class="{klass} bulk-edit-field-group">'
'<label class="field-toggle">'
'<input type="checkbox" name="_bulk" value="{name}" {checked}> {label}'
'</label>'
'<div class="field-content">'
'{html}'
'</div>'
'</div>'
).format(
klass=field_class or '',
name=name,
label=pgettext('form_bulk', 'change'),
checked='checked' if checked else '',
html=html
)
return html
class BulkEditFieldRenderer(BulkEditMixin, FieldRenderer):
layout = 'horizontal'
class InlineBulkEditFieldRenderer(BulkEditMixin, InlineFieldRenderer):
layout = 'inline'

View File

@@ -1,8 +1,9 @@
from datetime import timedelta
from datetime import datetime, timedelta
from urllib.parse import urlencode
from django import forms
from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
@@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
@@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm):
del self.fields['date_admission']
class NullBooleanSelect(forms.NullBooleanSelect):
def __init__(self, attrs=None):
choices = (
('unknown', _('Keep the current values')),
('true', _('Yes')),
('false', _('No')),
)
super(forms.NullBooleanSelect, self).__init__(attrs, choices)
class SubEventBulkEditForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.mixed_values = kwargs.pop('mixed_values')
self.queryset = kwargs.pop('queryset')
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['rows'] = '3'
for k in ('name', 'location', 'frontpage_text'):
# i18n fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
else:
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].one_required = False
for k in ('geo_lat', 'geo_lon'):
# scalar fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
else:
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].widget.is_required = False
self.fields[k].required = False
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'):
self.fields[k + '_day'] = forms.DateField(
label=self._meta.model._meta.get_field(k).verbose_name,
help_text=self._meta.model._meta.get_field(k).help_text,
widget=DatePickerWidget(),
required=False,
)
self.fields[k + '_time'] = forms.TimeField(
label=self._meta.model._meta.get_field(k).verbose_name,
help_text=self._meta.model._meta.get_field(k).help_text,
widget=TimePickerWidget(),
required=False,
)
class Meta:
model = SubEvent
localized_fields = '__all__'
fields = [
'name',
'location',
'frontpage_text',
'geo_lat',
'geo_lon',
'is_public',
'active',
]
field_classes = {
}
widgets = {
}
def save(self, commit=True):
objs = list(self.queryset)
fields = set()
check_map = {
'geo_lat': '__geo',
'geo_lon': '__geo',
}
for k in self.fields:
cb_val = self.prefix + check_map.get(k, k)
if cb_val not in self.data.getlist('_bulk'):
continue
if k.endswith('_day'):
for obj in objs:
oldval = getattr(obj, k.replace('_day', ''))
cval = self.cleaned_data[k]
if cval is None:
newval = None
if not self._meta.model._meta.get_field(k.replace('_day', '')).null:
continue
elif oldval:
oldval = oldval.astimezone(self.event.timezone)
newval = oldval.replace(
year=cval.year,
month=cval.month,
day=cval.day,
)
else:
# If there is no previous date/time set, we'll just set to midnight
# If the user also selected a time, this will be overridden anyways
newval = datetime(
year=cval.year,
month=cval.month,
day=cval.day,
tzinfo=self.event.timezone
)
setattr(obj, k.replace('_day', ''), newval)
fields.add(k.replace('_day', ''))
elif k.endswith('_time'):
for obj in objs:
# If there is no previous date/time set and only a time is changed not the
# date, we instead use the date of the event
oldval = getattr(obj, k.replace('_time', '')) or obj.date_from
cval = self.cleaned_data[k]
if cval is None:
continue
oldval = oldval.astimezone(self.event.timezone)
newval = oldval.replace(
hour=cval.hour,
minute=cval.minute,
second=cval.second,
)
setattr(obj, k.replace('_time', ''), newval)
fields.add(k.replace('_time', ''))
else:
fields.add(k)
for obj in objs:
setattr(obj, k, self.cleaned_data[k])
if fields:
SubEvent.objects.bulk_update(objs, fields, 200)
def full_clean(self):
if len(self.data) == 0:
# form wasn't submitted
self._errors = ErrorDict()
return
super().full_clean()
class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
@@ -162,15 +300,32 @@ class SubEventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
self.default = kwargs.pop('default', None)
self.disabled = kwargs.pop('disabled', False)
super().__init__(*args, **kwargs)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
label=self.property.name,
choices=[
('', _('Default ({value})').format(value=self.default or self.property.default) if self.default or self.property.default else ''),
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
)
else:
self.fields['value'].label = self.property.name
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
self.fields['value'].required = False
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['slug']
class Meta:
model = SubEventMetaValue

View File

@@ -5,7 +5,7 @@ from io import StringIO
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator
from django.db.models.functions import Lower
from django.db.models.functions import Upper
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
@@ -346,8 +346,8 @@ class VoucherBulkForm(VoucherForm):
data = super().clean()
vouchers = self.instance.event.vouchers.annotate(
code_lower=Lower('code')
).filter(code_lower__in=[c.lower() for c in data['codes']])
code_upper=Upper('code')
).filter(code_upper__in=[c.upper() for c in data['codes']])
if vouchers.exists():
raise ValidationError(_('A voucher with one of these codes already exists.'))
@@ -377,26 +377,5 @@ class VoucherBulkForm(VoucherForm):
return data
def save(self, event, *args, **kwargs):
objs = []
for code in self.cleaned_data['codes']:
obj = modelcopy(self.instance)
obj.event = event
obj.code = code
try:
obj.seat = self.cleaned_data['seats'].pop()
obj.item = obj.seat.product
except IndexError:
pass
data = dict(self.cleaned_data)
data['code'] = code
data['bulk'] = True
del data['codes']
objs.append(obj)
Voucher.objects.bulk_create(objs, batch_size=200)
objs = []
for v in event.vouchers.filter(code__in=self.cleaned_data['codes']):
# We need to query them again as bulk_create does not fill in .pk values on databases
# other than PostgreSQL
objs.append(v)
return objs
def post_bulk_save(self, objs):
pass

View File

@@ -273,8 +273,15 @@ def _display_checkin(event, logentry):
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
plains = {
'pretix.object.cloned': _('This object has been created by cloning.'),
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.webhook.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'),
'pretix.event.deleted': _('An event has been deleted.'),
'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),

View File

@@ -427,6 +427,13 @@ def get_organizer_navigation(request):
}),
'active': url.url_name == 'organizer.edit',
},
{
'label': _('Event metadata'),
'url': reverse('control:organizer.properties', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name.startswith('organizer.propert'),
},
]
})
if 'can_change_teams' in request.orgapermset:

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