Compare commits

...

410 Commits

Author SHA1 Message Date
Raphael Michel
a928d8c781 Bump version to 3.4.0 2019-12-06 15:35:44 +01:00
Raphael Michel
27b7a756fb Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-12-06 15:07:44 +01:00
Raphael Michel
7252167cc7 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-12-06 15:07:44 +01:00
Raphael Michel
d6ac337bde Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3360 of 3360 strings)

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

powered by weblate
2019-12-06 15:07:44 +01:00
Raphael Michel
aa9d3ab614 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3360 of 3360 strings)

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

powered by weblate
2019-12-06 15:07:44 +01:00
Martin Gross
186fb851e6 Translated on translate.pretix.eu (Russian)
Currently translated at 33.5% (1127 of 3360 strings)

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

powered by weblate
2019-12-06 13:07:26 +01:00
Martin Gross
97f7507dad Translated on translate.pretix.eu (Latvian)
Currently translated at 33.5% (1124 of 3360 strings)

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

powered by weblate
2019-12-06 13:07:19 +01:00
Martin Gross
e4582dd9ed Add itemcount (Items, excluding AddOns) to fragment_cart (#1514)
* Add itemcount (Items, excluding AddOns) to fragment_cart

* Simplify itemcount
2019-12-05 16:45:41 +01:00
Raphael Michel
090d5bfe94 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-12-05 14:34:15 +01:00
Martin Gross
b4e98e21a1 Translated on translate.pretix.eu (Russian)
Currently translated at 33.8% (1124 of 3323 strings)

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

powered by weblate
2019-12-05 14:20:29 +01:00
Carolina Fernández
4571abe496 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-12-05 14:20:20 +01:00
Carolina Fernández
f86520af37 Translated on translate.pretix.eu (Spanish)
Currently translated at 92.5% (3074 of 3323 strings)

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

powered by weblate
2019-12-05 14:20:15 +01:00
Jaroslav Lichtblau
1a5b8a2e07 Translated on translate.pretix.eu (Czech)
Currently translated at 0.2% (8 of 3323 strings)

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

powered by weblate
2019-12-05 14:20:07 +01:00
Maico Timmerman
dddf91d3bf Feature/optional team provisioning (#1487)
* Event creation: Add option to select existing team instead of provisioning.

* Event creation: disallow event team provisioning on organizer level.

* Event team provisioning: update default setting location and display strings.

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

Co-Authored-By: Raphael Michel <mail@raphaelmichel.de>
2019-12-05 12:20:40 +01:00
Martin Gross
f88a09a78c Update reportlab requirement 2019-12-05 10:43:38 +01:00
Raphael Michel
adebcc31d4 Call order_paid when creating paid orders through the API 2019-12-05 10:02:46 +01:00
Raphael Michel
5645a07cdc Email design: Make height of separator more reliable 2019-12-05 10:02:27 +01:00
Martin Gross
1d732e9675 Update pillow requirement from 5.* to 6.* 2019-12-04 18:47:18 +01:00
Martin Gross
61de010bbb Put customer VAT ID in the same line as the descriptor 2019-12-04 17:55:13 +01:00
Martin Gross
22eb9d3493 Add "Are you sure?"-dialog to dirty forms (#1506)
* Add "Are you sure?"-dialog to dirty forms

* Better form detection
2019-12-04 13:59:08 +01:00
Raphael Michel
4e8bda0c96 Improve email contents (#1510)
Improve email contents
2019-12-04 13:58:20 +01:00
Raphael Michel
0c83e5a50c Use a narrower separator PNG to reduce wrong-looking area in dark mode 2019-12-04 12:23:45 +01:00
Raphael Michel
745d7f74a1 Fix a display issue in GMail 2019-12-04 11:56:14 +01:00
Raphael Michel
8c86169d3f Fix #1415 -- Proper MIME structure 2019-12-04 11:56:02 +01:00
Raphael Michel
3c87272fdc Bank transfer import: Fix crash when no date column is selected 2019-12-04 10:29:26 +01:00
Raphael Michel
f1142560f6 Fix crash with specific chromium user agents 2019-12-04 10:29:18 +01:00
Raphael Michel
d46278f04f Set cookies with SameSite=None if possible (#1509) 2019-12-03 14:50:18 +01:00
Raphael Michel
098b7363e6 Waren about products with hidden_if_available 2019-12-01 12:34:47 +01:00
Raphael Michel
11d0c37415 Try to discourage accidentally setting Item.hidden_if_available 2019-12-01 12:31:09 +01:00
Raphael Michel
99607aa74a Fix AttributeError in sendmail form
PRETIXEU-1NQ
2019-12-01 11:17:30 +01:00
Raphael Michel
d44b75388e RelativeDate: Fix parsing of invalid strings
PRETIXEU-1NH
2019-11-29 10:29:53 +01:00
Raphael Michel
531c8aedc2 Remove logging statement 2019-11-28 16:32:02 +01:00
Martin Gross
0474651070 Add unlimited_items_per_order-flag to Sales Channels (#1508)
* Add unlimited_items_per_order-flag to Sales Channels

* Test for unlimited_items_per_order Sales Channels-flag

* Fix test
2019-11-28 16:31:38 +01:00
Raphael Michel
fd7ad3cb16 Fix docs typo 2019-11-28 15:10:26 +01:00
Raphael Michel
d5b932e0d9 Add data-fix option to widget 2019-11-28 14:26:14 +01:00
Raphael Michel
5462e256ac Allow to create a new gift card when refunding 2019-11-28 13:50:10 +01:00
Raphael Michel
46d4d97c13 Add BasePaymentProvider.refund_control_render 2019-11-28 13:50:10 +01:00
Raphael Michel
8b5241d520 Improve error messages when changing seating plans 2019-11-28 12:12:12 +01:00
Raphael Michel
ee06844b0d Allow to sort the list of vouchers 2019-11-28 11:29:33 +01:00
Raphael Michel
d4a491fc1b Add new signal pretix.control.signals.order_search_filter_q 2019-11-28 11:18:09 +01:00
Raphael Michel
bb3348022f Allow to configure help text of email field 2019-11-28 11:11:55 +01:00
Raphael Michel
a10082a46f CSV exports: Subevent name and date in separate columns 2019-11-28 10:24:12 +01:00
Raphael Michel
ef555eaff1 Fix new tests on real databases 2019-11-27 17:40:42 +01:00
Raphael Michel
31adc37599 API: Add convenience methods to make it easier to create fees 2019-11-27 17:14:44 +01:00
Raphael Michel
1d02764be2 Do not show "other fee" text on invoices 2019-11-27 16:55:53 +01:00
Raphael Michel
f80f2e9bf9 Show "choose other event date" button even in iframes 2019-11-27 16:09:46 +01:00
Raphael Michel
f5e7e0e309 Fix crash if Bundle.designated_price is set to null 2019-11-27 16:07:22 +01:00
Raphael Michel
bbc70447a2 Allow to create vouchers for *all* products (#1504) 2019-11-27 14:57:09 +01:00
Raphael Michel
530632d624 Add invoice_line_text to documentation 2019-11-26 17:15:13 +01:00
Raphael Michel
370130f047 Add signal invoice_line_text 2019-11-26 16:41:41 +01:00
Raphael Michel
14575693b8 Add signal item_description 2019-11-26 16:36:25 +01:00
Raphael Michel
436dcc68f2 Show link for other dates in widget checkout 2019-11-25 16:44:42 +01:00
Raphael Michel
b49e02d988 Voucher redemption: Disable add-on-cart button if no products are
selected
2019-11-25 15:54:11 +01:00
Raphael Michel
037644421b Question statistics: Do not include canceled orders 2019-11-25 15:42:37 +01:00
Raphael Michel
654faa5a10 Order list: Filter by answers should be case-sensitive 2019-11-25 15:42:37 +01:00
Raphael Michel
53a22e0e88 Expiry warnings: Pre-fetch expiry column 2019-11-24 16:27:47 +01:00
Raphael Michel
f8a080d180 Refs #1289 -- Download reminders for subevents and download reminder performance 2019-11-24 16:05:03 +01:00
Raphael Michel
b013737d70 Fix AttributeError introduced in last commit 2019-11-24 15:40:14 +01:00
Raphael Michel
24d6816dac Improve performance of send_expire_warnings and expire_orders 2019-11-24 15:28:03 +01:00
Maico Timmerman
2eb92bef9b Send widget_data when opening waiting list (#1502) 2019-11-24 11:33:56 +01:00
Raphael Michel
8d5b4d190c Fix TypeError introduced in 50dabb5b2 2019-11-22 16:10:58 +01:00
Raphael Michel
50dabb5b26 Show seat in web and PDF check-in list 2019-11-22 13:32:39 +01:00
Raphael Michel
84fb25e4d9 Voucher creation: Auto-select products based on seat 2019-11-22 13:25:20 +01:00
Raphael Michel
ee4f75c2fb Introduce Seat.sorting_rank (#1499)
* Introduce Seat.sorting_rank

* Fix comments

* Comments, for real
2019-11-22 13:25:08 +01:00
Raphael Michel
1e63eb743c Merge pull request #1498 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-11-22 13:24:21 +01:00
Mie Frydensbjerg
09477e1431 Translated on translate.pretix.eu (Danish)
Currently translated at 48.1% (1600 of 3323 strings)

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

powered by weblate
2019-11-21 17:00:11 +00:00
Raphael Michel
2be49d8cab Bank transfer: Do not cause duplicate payments 2019-11-21 17:22:11 +01:00
Raphael Michel
808ccfee75 Bank import: useful matching of negative transactions
Negative transactions are never matched automatically, which does not
change. However, when matching them manually, a negative payment was
created, which does not make much sense. Now, if a negative payment is
manually matched, the system checks whether:

a) There is a manual refund in pending state. In this case, the refund
   will be marked as done.

b) There is a bank transfer payment of the same amount, in which case
   this is handled like a credit card chargeback (i.e. notification on
   the dashboard, ...)

c) Otherwise, an error will be returned asking the user to create a
   refund object manually.
2019-11-20 18:35:15 +01:00
Raphael Michel
485766e247 Sendmail plugins: Fix issues around history of checkin-filtered emails 2019-11-20 18:08:22 +01:00
Martin Gross
bab27f5ab6 Respect payment-saleschannel restrictions also on order change 2019-11-20 17:18:51 +01:00
Martin Gross
26141da1b3 Update from Weblate (#1497)
Currently translated at 70.9% (73 of 103 strings)

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

powered by weblate

Co-authored-by: Mie Frydensbjerg <mif@aarhus.dk>
2019-11-20 15:42:27 +01:00
Mie Frydensbjerg
4c6cf63d68 Translated on translate.pretix.eu (Danish)
Currently translated at 70.9% (73 of 103 strings)

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

powered by weblate
2019-11-20 14:41:46 +00:00
Martin Gross
24fa251b05 Update from Weblate (#1496)
Update from Weblate

Co-authored-by: Mie Frydensbjerg <mif@aarhus.dk>
2019-11-20 15:01:16 +01:00
Mie Frydensbjerg
d65eaf7b95 Translated on translate.pretix.eu (Danish)
Currently translated at 48.1% (1598 of 3323 strings)

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

powered by weblate
2019-11-20 13:59:25 +00:00
Mie Frydensbjerg
e24a7f3887 Translated on translate.pretix.eu (Danish)
Currently translated at 69.9% (72 of 103 strings)

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

powered by weblate
2019-11-20 13:54:08 +00:00
Martin Gross
ec66b20798 Fix ToC for docs 2019-11-20 12:55:19 +01:00
Martin Gross
2c3c9a4ba1 Merge pull request #1495 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-11-20 10:04:41 +01:00
Martin Gross
4c8e1cef7f Translated on translate.pretix.eu (Russian)
Currently translated at 33.8% (1123 of 3323 strings)

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

powered by weblate
2019-11-20 09:02:33 +00:00
Raphael Michel
180963c787 Widget: Auto-open seating plan in some situations 2019-11-19 22:48:47 +01:00
Raphael Michel
ee96fb2ca1 Fix widget test cases 2019-11-19 22:42:51 +01:00
Raphael Michel
5563259b11 Fix gettext import 2019-11-19 21:44:34 +01:00
Raphael Michel
e7b86e0deb Refactor attribution note in the widget 2019-11-19 21:16:04 +01:00
Raphael Michel
067c3b1abc Update from Weblate (#1494)
Update from Weblate
2019-11-19 17:00:48 +01:00
Maarten van den Berg
3fff04bcda Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3323 of 3323 strings)

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

powered by weblate
2019-11-19 15:55:59 +00:00
Maarten van den Berg
150ebc7ddb Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3323 of 3323 strings)

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

powered by weblate
2019-11-19 15:55:56 +00:00
Raphael Michel
2f906bae5c Translated on translate.pretix.eu (Latvian)
Currently translated at 33.7% (1120 of 3323 strings)

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

powered by weblate
2019-11-19 15:55:01 +00:00
Raphael Michel
82d1927fe7 Fix date range shown for one-day event 2019-11-19 16:16:08 +01:00
Raphael Michel
cde87b437a Update from Weblate (#1493)
Update from Weblate
2019-11-18 16:01:00 +01:00
Zane Smite
9cbf3490cf Translated on translate.pretix.eu (Latvian)
Currently translated at 33.7% (1120 of 3323 strings)

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

powered by weblate
2019-11-18 15:00:25 +00:00
Raphael Michel
56cfdfc5b3 Stripe: Only use alphanumeric characters in statement descriptors 2019-11-18 10:47:43 +01:00
Raphael Michel
8564d20183 Update from Weblate (#1492)
Update from Weblate
2019-11-18 10:46:11 +01:00
Raphael Michel
c3155ad16d Translated on translate.pretix.eu (Arabic)
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-11-18 09:44:59 +00:00
Raphael Michel
7fce062c2a Translated on translate.pretix.eu (Arabic)
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-11-18 09:41:05 +00:00
Raphael Michel
05311fda54 Update from Weblate (#1490)
Update from Weblate
2019-11-17 19:40:39 +01:00
Raphael Michel
0396c851ed Translated on translate.pretix.eu (Arabic)
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-11-17 18:35:20 +00:00
Zane Smite
1d2094f5ea Translated on translate.pretix.eu (Latvian)
Currently translated at 33.7% (1120 of 3323 strings)

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

powered by weblate
2019-11-17 18:35:20 +00:00
Maarten van den Berg
44b8531614 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.1% (3225 of 3323 strings)

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

powered by weblate
2019-11-17 18:35:20 +00:00
Raphael Michel
5e9610eecf Fix typo in function call 2019-11-17 19:35:06 +01:00
Raphael Michel
1c40351b27 Bulk voucher sending: Correctly strip lines of whitespace 2019-11-17 19:31:41 +01:00
Martin Gross
3a4196c087 Remove seating-icon filler to stop Chrome complaining about the CSP for inline-styling 2019-11-15 20:53:20 +01:00
Martin Gross
080b5f02cb Fix FAQ on linear barcodes spelling even more, ffs... 2019-11-15 19:21:30 +01:00
Martin Gross
21c052918c Fix FAQ on linear barcodes spelling 2019-11-15 18:53:58 +01:00
Raphael Michel
9515249098 Bulk voucher creation: Fix spaces in seat IDs 2019-11-15 18:44:47 +01:00
Martin Gross
a0dd495cc3 FAQ on linear barcodes 2019-11-15 16:27:07 +01:00
Martin Gross
46ce9904cd Fix documentation for override_layout signal 2019-11-15 15:54:50 +01:00
Martin Gross
600310da0f Fix override_layout signal 2019-11-15 12:12:13 +01:00
Martin Gross
3b306de1bb New signal: ticketoutput_override_layout (#1483)
* Add signal to override ticketoutput layout

* Change Signal collection to send_chained

* Move ticketoutput_override_layout to ticketoutputpdf-plugin
2019-11-15 11:18:52 +01:00
Raphael Michel
a2c1c69d7e Seat-specific vouchers (#1486)
* Basic functionality

* API

* Do not delete seats with vouchers

* Show seat in list of seats

* Validate availability of seats

* Fix invalid logic in Seat.is_available

* Show voucher name in edit form
2019-11-15 10:56:34 +01:00
Raphael Michel
f79df47b78 Add specific social media image 2019-11-15 10:33:55 +01:00
Raphael Michel
1703f3d636 Update from Weblate (#1489)
Update from Weblate
2019-11-15 09:47:48 +01:00
Raphael Michel
580ae7a2e9 Translated on translate.pretix.eu (Latvian)
Currently translated at 33.5% (1113 of 3323 strings)

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

powered by weblate
2019-11-15 08:42:30 +00:00
Raphael Michel
34703cf6de Translated on translate.pretix.eu (Russian)
Currently translated at 33.8% (1123 of 3323 strings)

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

powered by weblate
2019-11-15 08:32:53 +00:00
Raphael Michel
1999495638 Translated on translate.pretix.eu (Russian)
Currently translated at 33.8% (1123 of 3323 strings)

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

powered by weblate
2019-11-15 08:30:51 +00:00
Raphael Michel
00798d9207 Payment providers: Add priority flag 2019-11-15 09:29:38 +01:00
Raphael Michel
ac2ff6c2aa Phone number fields: Do not crash on invalid default value 2019-11-15 08:48:14 +01:00
Raphael Michel
35ac2f0d2b Merge pull request #1482 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-11-14 12:38:43 +01:00
Raphael Michel
3432bb089a Translated on translate.pretix.eu (Latvian)
Currently translated at 33.4% (1110 of 3323 strings)

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

powered by weblate
2019-11-14 11:37:41 +00:00
Zane Smite
69b03b24a1 Translated on translate.pretix.eu (Latvian)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-11-14 11:12:30 +00:00
Raphael Michel
520dd3e254 Refunds: Do not try to cancel canceled order 2019-11-14 12:12:15 +01:00
Raphael Michel
d98fce3594 Fix cancelling payments through the API 2019-11-14 12:10:45 +01:00
Raphael Michel
aa5ae8b2bd Do not crash because of invalid phone numbers in the database 2019-11-14 11:36:31 +01:00
Raphael Michel
b148182240 Cancel payments when cancelling order 2019-11-14 10:42:33 +01:00
Raphael Michel
339d7f06ed Payment provider API: Add method cancel_payment 2019-11-14 10:39:54 +01:00
Raphael Michel
b876293453 Fix problem when cancelling an order with invoices with different prefixes 2019-11-13 14:14:14 +01:00
Martin Gross
54091b9721 Add question type: phone number (#1462)
* Add Phonenumber-Field as to Questions

* Add setup requirements

* Add list of ask-during-checkin restricted question types and enforce it

* Fix requirements

* Fix crash using custom locales

* Re-format phone numbers when outputting to humans

* Initialize country code field with a guess for the customer's country

* Document TEL type in API docs
2019-11-13 12:52:07 +01:00
Raphael Michel
06a744ea2d Refactor cart management into new helper function cached_invoice_address 2019-11-13 11:42:03 +01:00
Raphael Michel
c24ab642ce Fix logic bug introduced in 0e9d2cfc1 during form validation 2019-11-13 10:30:29 +01:00
Raphael Michel
2ab3e1d420 Explicitly write Latvian with L 2019-11-13 10:08:51 +01:00
Raphael Michel
0e9d2cfc10 Require certain parts of names if names are required 2019-11-12 18:15:25 +01:00
Raphael Michel
63a46d0156 Fix russian translation 2019-11-12 17:28:49 +01:00
Martin Gross
6896682dd1 Restrict Payment Providers to Sales Channels (#1481)
* Allow to restrict payment providers to specific sales channels

* Fix test

* Add `payment_restrictions_supported`-property to SalesChannels
2019-11-12 17:11:43 +01:00
Raphael Michel
384e7f8fc1 Update from Weblate (#1479)
Update from Weblate
2019-11-12 17:11:29 +01:00
Mie Frydensbjerg
d403b3d433 Translated on translate.pretix.eu (Danish)
Currently translated at 48.0% (1595 of 3323 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Zane Smite
552e523229 Translated on translate.pretix.eu (Latvian)
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Maarten van den Berg
95ca6da74a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3323 of 3323 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Zane Smite
bce8879804 Translated on translate.pretix.eu (Latvian)
Currently translated at 49.5% (51 of 103 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Raphael Michel
a05cce8af7 Translated on translate.pretix.eu (Russian)
Currently translated at 97.1% (100 of 103 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Zane Smite
dd6f1ee70f Translated on translate.pretix.eu (Latvian)
Currently translated at 10.7% (11 of 103 strings)

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

powered by weblate
2019-11-12 10:11:34 +00:00
Raphael Michel
ad91ed4a30 Correctly deal with tax rules that used to have custom rules 2019-11-12 11:11:21 +01:00
Raphael Michel
e3c3154469 Fix invoice duplication when importing payments for expired orders 2019-11-11 18:59:33 +01:00
Raphael Michel
54cd38bbaa Always render product names bold 2019-11-11 17:13:54 +01:00
Raphael Michel
8dbba1cfd3 Order details: Do not show download note when disabled on product level 2019-11-11 12:38:33 +01:00
Raphael Michel
0bf3ee1a6f Refactor into method in ClassicInvoiceRenderer 2019-11-10 13:07:06 +01:00
Raphael Michel
da7e1dee3e Offset refunds: Catch exceptions in the right place 2019-11-10 12:59:10 +01:00
Raphael Michel
df3cc1499f Fix #1480 -- Remove apparently obsolete code 2019-11-08 16:13:23 +01:00
Raphael Michel
2d066e3b5e Add CSS required for font preview to organizer settings 2019-11-08 09:19:27 +01:00
Raphael Michel
25cfa8973c API: Add invoice line position to invoice object 2019-11-07 15:43:22 +01:00
Raphael Michel
3f7f04ed21 Enable Russian and Latvian languages 2019-11-07 13:50:41 +01:00
Raphael Michel
88b2f4738a Ticket preview: use an existing subevent 2019-11-07 13:06:01 +01:00
Raphael Michel
ea71b1f04e Fix typo 2019-11-06 15:42:20 +01:00
Raphael Michel
03a06b6997 Fix wrong attribute name 2019-11-06 15:42:12 +01:00
Raphael Michel
1cd319357d Add "Invoice" to the filename of attached invoices 2019-11-06 14:10:42 +01:00
Raphael Michel
4c653aa4f1 Bump to 3.4.0.dev0 2019-11-06 14:05:54 +01:00
Raphael Michel
5b4e27c47b Bump version to 3.3.0 2019-11-06 13:10:40 +01:00
Raphael Michel
919afb94c4 Fix outdated API documentation 2019-11-05 19:00:09 +01:00
Raphael Michel
8f112f8d9a Pass cart positions to fee_calculation_for_cart 2019-11-04 11:00:48 +01:00
Raphael Michel
d15e37d93e Do not show availability for currently-selected add-on 2019-11-04 09:55:00 +01:00
Raphael Michel
47cf019079 Make voucher send task transaction-aware 2019-11-04 09:18:23 +01:00
Raphael Michel
f51521143b PDF: Fix incorrect type of position id 2019-11-02 12:33:29 +01:00
Raphael Michel
a0ca76f0ec Fix incorrect offset payments when changing and splitting orders at the same time 2019-11-02 11:15:26 +01:00
Raphael Michel
d7f71948c1 Fix typo in docstring of a signal 2019-11-01 14:30:51 +01:00
Raphael Michel
7d72611851 Allow to add order position number to PDFs 2019-11-01 14:30:51 +01:00
Raphael Michel
2e9e5a8994 Fix untranslated email subject 2019-10-31 16:00:44 +01:00
Raphael Michel
c8289877bb Update from Weblate (#1476)
Update from Weblate
2019-10-31 15:40:39 +01:00
Raphael Michel
bb0ad6fb11 Translated on translate.pretix.eu (Turkish)
Currently translated at 77.9% (2587 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:48 +00:00
Raphael Michel
6dcdb87725 Translated on translate.pretix.eu (Spanish)
Currently translated at 90.2% (2997 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:47 +00:00
Raphael Michel
c2c8f36724 Translated on translate.pretix.eu (Greek)
Currently translated at 91.8% (3051 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:46 +00:00
Raphael Michel
11c0a38665 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3323 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:45 +00:00
Raphael Michel
c1c5388923 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3323 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:43 +00:00
Raphael Michel
c7054fe72d Translated on translate.pretix.eu (French)
Currently translated at 69.7% (2317 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:42 +00:00
Raphael Michel
13be3b7899 Translated on translate.pretix.eu (Danish)
Currently translated at 47.5% (1580 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:41 +00:00
Raphael Michel
d9fe3e51da Translated on translate.pretix.eu (Catalan)
Currently translated at 31.4% (1044 of 3323 strings)

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

powered by weblate
2019-10-31 14:39:40 +00:00
telegnom
e381e50fbf missing dot in docker command (#1474) 2019-10-30 18:00:33 +01:00
Raphael Michel
73a323528b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-30 17:23:53 +01:00
Raphael Michel
3a0aaa3f92 Stripe: Relax same-session requirement for iDEAL payments
This security mechanism caused problems with banking apps who open the
return view in a separate browser session inside the banking app.
2019-10-30 17:23:11 +01:00
Martin Gross
2a9c105e51 PayPal: Show payment ID on paid invoices (#1471)
* Always call render_invoice_text - even if order is already paid

* Print PayPal payment ID on invoice if available and invoice is paid

* Also display Sale ID on invoice

* try/except for paymentId/SaleId
2019-10-30 10:48:15 +01:00
Raphael Michel
038533ad63 Allow to change fees in existing orders (#1472)
* Allow to change fees in existing orders

* Add tests

* Add special case for payment options

* Fix PK reference in tests
2019-10-29 22:04:42 +01:00
Raphael Michel
fcf9f0054e Fix #1328 -- Show internal name in list of categories 2019-10-29 20:01:35 +01:00
Raphael Michel
621b0c8c95 Fix #1390 -- Fix string on frontpage being evaluated before translations are loaded 2019-10-29 20:00:00 +01:00
Raphael Michel
4a40312a32 Fix #1377 -- Copy has_subevents when cloning events through the API 2019-10-29 19:54:45 +01:00
Raphael Michel
b156efaae8 Fix #1468 -- Fix sorting by fallback name parts in check-in lists 2019-10-29 19:53:52 +01:00
Felix Schäfer
f473439f77 PDF renderer: Properly escape HTML answer fields (#1473) 2019-10-29 17:58:10 +01:00
Raphael Michel
9ed49fb379 PDF renderer: Properly escape HTML in some fields 2019-10-29 12:10:30 +01:00
Martin Gross
70e95b42fa Add manual order-matching field for duplicate bankimport transactions (#1461)
* Add manual order-matching field for duplicate bankimport transactions

* Only show matching-field for already matched transactions - not for errored ones.
2019-10-29 11:54:36 +01:00
Raphael Michel
1e0e8184c8 Fix #312 -- Bulk-send vouchers via email (#1469)
* Allow to directly bulk-send vouchers via email

* Send mails

* Log messages

* Fix test failures

* Add new test cases
2019-10-29 11:53:59 +01:00
Raphael Michel
161f4a8132 Update from Weblate (#1467)
Update from Weblate
2019-10-29 11:51:13 +01:00
Raphael Michel
8ac978062f Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3302 of 3302 strings)

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

powered by weblate
2019-10-29 10:50:30 +00:00
Raphael Michel
619435d0f6 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3302 of 3302 strings)

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

powered by weblate
2019-10-29 10:50:29 +00:00
Maarten van den Berg
7e9e03cef4 Translated on translate.pretix.eu (Dutch)
Currently translated at 97.5% (3219 of 3302 strings)

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

powered by weblate
2019-10-29 08:42:26 +00:00
Raphael Michel
dc3e76d6d3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-29 09:42:15 +01:00
Raphael Michel
df2c9bdce5 Adjust strings in tests 2019-10-29 09:41:33 +01:00
Raphael Michel
20a6decb3a Notifications: Add details about subevents and products 2019-10-28 22:54:56 +01:00
Raphael Michel
3d31b95201 Event list: Autocomplete meta values 2019-10-28 22:35:16 +01:00
Raphael Michel
cc970caad8 Allow to filter global event list using organizer parameters 2019-10-28 22:03:09 +01:00
Raphael Michel
845f231446 Replace "Tickets on sale" with "Book now" in frontend 2019-10-28 21:46:33 +01:00
Raphael Michel
538d1b6b58 Adjust order of events on dashboard 2019-10-28 17:01:05 +01:00
Raphael Michel
601fefc57f Remove any doubt from dashboard order 2019-10-28 16:58:32 +01:00
Raphael Michel
645b9696b7 Increase length of OAuthGrant.redirect_uri 2019-10-28 16:42:17 +01:00
Raphael Michel
ee5ce900d2 Update from Weblate (#1460)
Update from Weblate
2019-10-25 17:04:58 +02:00
Martin Gross
ab0be57106 API: Allow to filter events by attributes (#1466)
* Allow filtern events-ressource on API by attrs

* Document meta-data attr-filter for order resource

* Actually document the change and not some random other nonsensical thing...

* Doc for subevent-filtering

* Test for attr-filtering on events resource

* Allow attr-filtering on subevents

* Add attr-filter test for subevent

* Update doc/api/resources/subevents.rst

* Update src/tests/api/test_subevents.py

* Update doc/api/resources/events.rst
2019-10-25 17:04:20 +02:00
oocf
ee6c4551b3 Translated on translate.pretix.eu (Spanish)
Currently translated at 90.9% (3000 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
yichengsd
d63956fe25 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 94.1% (3107 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
yichengsd
27fb9cd8b0 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 92.5% (3056 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
yichengsd
ada2de8bc9 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 92.2% (3046 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
yichengsd
7fe3ac3b5e Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 91.6% (3026 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
yichengsd
13cf24d7fa Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 90.3% (2981 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
Felix Rindt
6d95603aa8 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3302 of 3302 strings)

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

powered by weblate
2019-10-25 14:50:08 +00:00
Raphael Michel
97fbec804b Stripe: Include event name in statement descriptors 2019-10-25 16:49:13 +02:00
Raphael Michel
8f16145dda Order creation API: Use correct language for email subjects 2019-10-25 09:02:38 +02:00
Raphael Michel
3f5e835367 Add safeguards and tests against duplicate cancellations 2019-10-24 16:07:59 +02:00
Tobias Kunze
fe92af10fb Ignore whitespace on typeahead queries (#1464)
Whitespace is often added when you copy/paste order or voucher codes
2019-10-24 10:29:57 +02:00
Martin Gross
5e88dcf329 Fix saleschannels testmode-support-message check 2019-10-23 17:54:07 +02:00
Raphael Michel
d7c8460c28 Fix failing widget tests 2019-10-23 17:40:33 +02:00
Raphael Michel
837775c8d4 Correctly respect mail_attach_ical setting on order payments 2019-10-23 16:17:39 +02:00
Raphael Michel
2a51969b04 Add (hidden) location field to event list widget 2019-10-23 15:28:15 +02:00
Martin Gross
357972c8f8 Rearrange order_gracefully_delete signal-call (#1463) 2019-10-23 15:17:46 +02:00
Raphael Michel
08460f9918 Add loading callback to widget 2019-10-23 15:09:20 +02:00
Raphael Michel
277b7ad71c Hide manual refunds from users as well 2019-10-23 14:39:21 +02:00
Raphael Michel
d0340044d3 Order transition: Deal with empty cancellation fee 2019-10-22 17:58:51 +02:00
Raphael Michel
8abaa16fab Change log level of widget loading 2019-10-22 10:40:14 +02:00
Raphael Michel
5e4a16bd44 Allow to filter event list in organizer view by meta data 2019-10-21 19:03:51 +02:00
Raphael Michel
b219faafaa Work around a potential Django bug on MySQL
On MySQL, we observe that the preloaded items are missing everywhere
except the first time they occur.
2019-10-21 16:13:48 +02:00
Martin Gross
3006d35622 Skip ticket-attachment for text/uri-list 2019-10-21 16:00:42 +02:00
Martin Gross
a879a86dff Remove unused property 2019-10-21 15:02:44 +02:00
Raphael Michel
926b94c88f Update from Weblate (#1459)
Update from Weblate
2019-10-21 14:43:56 +02:00
Raphael Michel
638f1324fe Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3302 of 3302 strings)

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

powered by weblate
2019-10-21 12:43:10 +00:00
Raphael Michel
be7fea4e2e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3302 of 3302 strings)

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

powered by weblate
2019-10-21 12:42:57 +00:00
Raphael Michel
adf7b6a4ed Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-21 14:07:43 +02:00
pajowu
fc4c6444cb Sendmail plugin: Allow filtering for check-ins (#1382)
* Allow filtering mass-emails for Checkin Lists

* Allow filtering mass emails for not checked in

* Fix email filtering logic issue

* Use Select2 for checkin lists selection

* sendmail plugin: Make checkin list filtering optional

* Remove unused constant

* Re-size panel to only fit the right column

* Revert incorrect JavaScript change

* Change semantics of not_checked_in

* Introduce a subquery to filter on position properties
2019-10-21 14:06:11 +02:00
Martin Gross
03c760c2bb Allow ticket output providers to handle downloads externally (#1402)
* TicketOutput-Providers: Make preview optional; download/attachable optional; optional specific target; update doc

* Spelling fixes in doc

* Changes after code-review

* Changes after code-review

* Commit missing template file

* Allow for redirects instead of files

* Return HTTPResponse with Content-Type text/uri-list on API

* Update API-doc

* Add viewable to spellinglist, fixing doc-test
2019-10-21 14:05:09 +02:00
Raphael Michel
27538d220e Fix #1416 -- Add canonical geodata field (#1458)
* Fix #1416 -- Add canonical geodata field for events and subevents

* Add optional geocoding through OpenCageData

* Fix markup everywhere

* Add Leaflet map to geo coordinates

* Fix tests, add credits

* Fix spelling
2019-10-21 13:07:35 +02:00
Raphael Michel
19b10e3ca4 Add option to attach calendar files to emails (#1457) 2019-10-21 10:41:22 +02:00
Martin Gross
2b18621c76 Add flag testmode_supported to sales channels (#1455)
* Add testmode-support-flag to SalesChannels

* Make saleschannels/testmode-warnings even more dangerous!

* Add warning for payment-methods that do support testmode but are being used in a non-testmode order caused by a saleschannel in a testmode-shop.

* Remove redundant testmode_supported-flag for WebshopSalesChannel

* Raise error on API when sales_channel does not support testmode

* Tests

* Fix style issue after merge
2019-10-21 10:07:02 +02:00
Raphael Michel
e8a2f7e349 Collapse gift card option when the gift card is not enough 2019-10-18 18:18:45 +02:00
Raphael Michel
a326ee8f75 Prevent creating negative gift code values 2019-10-18 17:54:23 +02:00
Raphael Michel
147b90b9e8 Update from Weblate (#1456)
Update from Weblate
2019-10-18 17:54:01 +02:00
Raphael Michel
1a461531eb Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3284 of 3284 strings)

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

powered by weblate
2019-10-18 15:51:52 +00:00
Raphael Michel
f854a0f8d6 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3284 of 3284 strings)

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

powered by weblate
2019-10-18 15:51:51 +00:00
Raphael Michel
137ed5da6a Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-18 15:23:12 +00:00
Raphael Michel
6695fef3ee Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-18 15:23:12 +00:00
Raphael Michel
4a8827cc2b Do not show "a refund is on its way" message for internal refunds 2019-10-18 17:22:50 +02:00
Raphael Michel
328b4ec633 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-18 17:17:48 +02:00
Raphael Michel
974a84fefe Merge pull request #1396 from pretix/giftcard
Support for gift cards
2019-10-18 17:08:45 +02:00
Raphael Michel
94b6b86696 Fix faulty test 2019-10-18 15:42:46 +02:00
Raphael Michel
8fe9b35dea Add more tests 2019-10-18 15:12:26 +02:00
Raphael Michel
2c87d5ece3 Update src/pretix/control/templates/pretixcontrol/organizers/giftcards.html
Co-Authored-By: Martin Gross <gross@rami.io>
2019-10-18 15:09:06 +02:00
Raphael Michel
f8433b5cc9 Add some tests 2019-10-18 13:08:25 +02:00
Martin Gross
0a200de41c Fix spelling in order_gracefully_delete signal 2019-10-18 12:07:33 +02:00
Martin Gross
25a998a510 Add order_gracefully_delete signal (#1454) 2019-10-18 11:38:26 +02:00
Raphael Michel
a1c7a6f2b0 Fix items test 2019-10-17 21:31:45 +02:00
Raphael Michel
1fe93ac6b7 Do not allow to pay gift cards with gift cards 2019-10-17 18:12:06 +02:00
Raphael Michel
4b2f25ce8a Add testmode for gift cards 2019-10-17 18:05:04 +02:00
Raphael Michel
33cee65d7f Add a tooltip with the date to pending orders 2019-10-17 17:27:33 +02:00
Raphael Michel
302966808e More docs and payments 2019-10-17 17:19:31 +02:00
Raphael Michel
767e679140 Show issued gift card in order view 2019-10-17 17:07:59 +02:00
Raphael Michel
1a07686b79 Merge branch 'giftcard' of github.com:pretix/pretix into giftcard 2019-10-17 17:00:10 +02:00
Raphael Michel
7b4c3a00a0 Update src/pretix/control/templates/pretixcontrol/organizers/giftcards.html
Co-Authored-By: Martin Gross <gross@rami.io>
2019-10-17 16:59:53 +02:00
Raphael Michel
7db87af754 Update src/pretix/control/templates/pretixcontrol/organizers/giftcards.html
Co-Authored-By: Martin Gross <gross@rami.io>
2019-10-17 16:59:44 +02:00
Raphael Michel
da9c30089a Fix sv translation 2019-10-17 16:40:46 +02:00
Raphael Michel
89a85392a9 Fixes 2019-10-17 16:39:42 +02:00
Raphael Michel
02b6cc5aad Update from Weblate (#1450)
Update from Weblate
2019-10-17 16:06:38 +02:00
Raphael Michel
b3e6f44027 Add double-spend safeguard 2019-10-17 16:04:22 +02:00
Raphael Michel
ac2df35db6 Allow configuring cross-organizer acceptance 2019-10-17 16:04:22 +02:00
Raphael Michel
ac212b798d Remove unneeded settings 2019-10-17 16:04:22 +02:00
Raphael Michel
195d418e00 Add spelling word 2019-10-17 16:04:22 +02:00
Raphael Michel
ba286d96cb Rebase and manually squash migrations 2019-10-17 16:04:22 +02:00
Raphael Michel
9842fcf7da Allow order change 2019-10-17 16:04:22 +02:00
Raphael Michel
e97ae04581 Helpful error messages 2019-10-17 16:04:22 +02:00
Raphael Michel
346f215c50 Refator payment provider, deal with cancellations 2019-10-17 16:04:22 +02:00
Raphael Michel
e099fad0ca Refator payment provider, deal with cancellations 2019-10-17 16:04:22 +02:00
Raphael Michel
4aeada0bfb Fix KeyError 2019-10-17 16:04:22 +02:00
Raphael Michel
73dd94fe73 Actually issue giftcards 2019-10-17 16:04:22 +02:00
Raphael Michel
8c50b7409f Add Item.issue_giftcard 2019-10-17 16:04:22 +02:00
Raphael Michel
b07d9d167d Add an API 2019-10-17 16:04:22 +02:00
Raphael Michel
f22d5915ea Handle refunds 2019-10-17 16:04:22 +02:00
Raphael Michel
e37d85f517 Cross-organizer acceptance 2019-10-17 16:04:22 +02:00
Raphael Michel
c68f715e07 Pending display 2019-10-17 16:04:22 +02:00
Raphael Michel
db71ec92be Payment step 2019-10-17 16:04:22 +02:00
Raphael Michel
85e7a16880 Backend management of gift cards 2019-10-17 16:04:22 +02:00
Raphael Michel
ed370fa913 Proof of concept 2019-10-17 16:04:22 +02:00
Raphael Michel
f7f00fe735 Data model 2019-10-17 16:04:22 +02:00
Tobias Sundgren
37b05fb4f4 Translated on translate.pretix.eu (Swedish)
Currently translated at 6.7% (217 of 3218 strings)

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

powered by weblate
2019-10-17 13:34:04 +00:00
Maarten van den Berg
defd00ffdf Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-17 13:34:04 +00:00
Maarten van den Berg
9f44d7bbf5 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-17 13:34:04 +00:00
Raphael Michel
28e27e91b2 Added translation on translate.pretix.eu (Latvian) 2019-10-17 13:34:04 +00:00
Raphael Michel
e519a50805 Added translation on translate.pretix.eu (Latvian) 2019-10-17 13:34:04 +00:00
Raphael Michel
b11b79b559 Update .travis.sh 2019-10-17 15:33:59 +02:00
Raphael Michel
88378be14e Add word to wordlist 2019-10-17 14:54:37 +02:00
Raphael Michel
8a8f8ae10a Fix KeyError in question_is_visible if question dependency is unknown 2019-10-17 12:57:17 +02:00
Raphael Michel
404d88a220 Fix #1451 -- Make event slug available in email templates 2019-10-17 11:50:01 +02:00
Raphael Michel
a48d461c22 Docs: Fix incorrect headline 2019-10-17 11:39:44 +02:00
Raphael Michel
5e59d41f6e Offset payment provider: Catch QuotaExceededException 2019-10-17 10:15:10 +02:00
Raphael Michel
7fb77eef34 Offset payment provider: Ignore payment term 2019-10-17 10:13:56 +02:00
Raphael Michel
f1321b67f9 Add missing docs file 2019-10-17 09:52:49 +02:00
Raphael Michel
8a6a515b6a Refs #775 -- Pluggable authentication backends (#1447)
* Drag-and-drop: Force csrf_token to be present

* Rough design

* Missing file

* b.visble

* Forms

* Docs

* Tests

* Fix variable
2019-10-17 09:11:03 +02:00
Martin Gross
e34511b984 Fix #1446 - Migrate NullBooleanSelect from 1/2/3 to unknown/true/false 2019-10-16 10:11:51 +02:00
Raphael Michel
fa3b40f53c Drag-and-drop: Force csrf_token to be present 2019-10-15 15:55:39 +02:00
Sohalt
b870dde301 Replace occurrences of "blacklist" with "banlist" (#1434)
* Rename blacklist to banlist

* Rename more cases of blacklist to banlist

* Rename Blacklist -> Banlist in migrations
2019-10-15 14:58:48 +02:00
Raphael Michel
a4d8c810ce Support for right-to-left languages (#1438)
* play around

* Flip things in presale

* Convert backend

* Remove test settings

* Safe getattr
2019-10-15 11:41:23 +02:00
Felix Schäfer
4152ee4e50 Fix #1408 -- Don't mark upload question fields as required if… (#1443) 2019-10-15 11:40:28 +02:00
Raphael Michel
7eb5b09b84 Merge remote-tracking branch 'origin/stripe-connect-fees' 2019-10-15 10:06:24 +02:00
Raphael Michel
2f7c16d18b Remove JavaScript lambda function (not supported in IE) 2019-10-15 10:04:17 +02:00
Raphael Michel
49bff3cc33 Fix field requirement display 2019-10-14 13:56:53 +02:00
Raphael Michel
80c9d1ff9e Allow requiring invoice name even if invoice address is required 2019-10-14 13:55:19 +02:00
Martin Gross
dc51d51338 Add hint to devices in checkinlist overview 2019-10-14 11:40:57 +02:00
Raphael Michel
8aa8a2265f Update from Weblate (#1441)
Update from Weblate
2019-10-14 11:24:49 +02:00
Maarten van den Berg
971275e774 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-14 08:54:27 +00:00
Sohalt
91b586ce08 Reordering questions with drag'n'drop (#1433)
* Reordering questions with drag'n'drop

* Add permission check

* Test permissions for question reordering

* Handle malformed requests for question reordering

* Show first up arrow and last down arrow

* Provide page offset

* Revert "Provide page offset"

This reverts commit 8090bd573f851a74cca442f4651c111b8750948d.

* Reorder questions endpoint with pagination support

* Rudimentary test for reordering endpoint

* Make reordering questions atomic

* cache questions

* Properly support pagination for reorder_questions

* appease linter

* Fix test
2019-10-14 10:54:20 +02:00
Johan von Forstner
f10d1bd236 fix typo (#1442) 2019-10-14 10:44:03 +02:00
Martin Gross
eafed2e213 Enforce that questions cannot depend on other question which are asked during checkin (Z#2352753) 2019-10-14 10:12:09 +02:00
Raphael Michel
02c81e0fa7 Stripe connect: Allow to set application fee 2019-10-13 13:30:58 +02:00
Raphael Michel
8e9a5e371c Fix documentation of Enterprise instlalations 2019-10-11 17:14:09 +02:00
Martin Gross
25345275c7 Link to ticket cancelation settings in timeline 2019-10-11 13:30:40 +02:00
Raphael Michel
b9a911dd97 Fix #1440 -- API confusion about creating free orders 2019-10-11 09:00:46 +02:00
Raphael Michel
361488f3e6 Bump to 3.3.0.dev0 2019-10-10 18:17:27 +02:00
Raphael Michel
b704d21e88 Bump to 3.2.0 2019-10-10 18:16:25 +02:00
Raphael Michel
4bfe0e3784 Order change manager: Allow to add multiple products 2019-10-10 12:59:16 +02:00
Raphael Michel
d4d046ca60 Order change manager: Allow to disable invoice issuing 2019-10-10 12:19:06 +02:00
Raphael Michel
fb3fc05522 Remove references to our legacy apps 2019-10-10 09:27:14 +02:00
Raphael Michel
fdcd750487 Disable autocomplete in context switcher 2019-10-09 17:16:47 +02:00
Raphael Michel
92754136a6 Refs #1432 -- Proper grouping of autocomplete properties 2019-10-09 12:40:05 +02:00
Raphael Michel
3b4d39ec27 Fix #1432 -- Correct autocomplete attributes of name part fields 2019-10-09 12:40:05 +02:00
Sohalt
88bb3b483c Only disable up arrow on first page and down arrow on last page (#1437) 2019-10-09 09:15:35 +02:00
Raphael Michel
247370839b Fix placeholder form validation 2019-10-09 08:58:05 +02:00
Raphael Michel
b0510f47b3 Move new option down a little 2019-10-08 17:43:28 +02:00
Raphael Michel
4fa086bbc5 Update from Weblate (#1435)
Update from Weblate
2019-10-08 16:09:46 +02:00
Raphael Michel
cb03e7c843 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-08 14:09:12 +00:00
Raphael Michel
d012d0804a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-08 14:09:11 +00:00
Raphael Michel
3731d5f431 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-08 15:57:05 +02:00
Raphael Michel
9f04d53564 Add safety mechanism for order code generation 2019-10-08 15:53:37 +02:00
Martin Gross
748a389acb Auto-check-in for specific sales channels (#1409)
* Autocheckin data model/cosmetics

* Expose automatically checked-in OrderPositions

* Expose automatically checked-in OrderPositions in CSV/PDF Exports

* Fix some tests, try to fix MultiStringField/CheckboxSelectMultiple

* Actually fix MultiStringField/CheckboxSelectMultiple.
(Not pretty, but it works)

* Fix more tests

* Squash migration

* Also fix CSV/nameparts-test

* Changes for Autocheckin code-review

* Perform Auto-Checkins through new core plugin

* Update config-doc to reflect also checkinlists

* Explicitly output AutoCheckin Yes/No for CSV-Export (+ fix test)

* Move autocheckin from plugin to service

* API-doc

* Fix API-doc spelling

* Checkinlist-API and autocheckin order tests

* Performance improvement when reading checkinlists for autocheckin

Co-Authored-By: Raphael Michel <michel@rami.io>

* Autocheckin test for order created through API

* Resolve migration conflict
2019-10-08 15:50:22 +02:00
Sohalt
05a1df244b Fix #1388 -- Prevent some words from occurring in order codes (#1422)
* prevent some words from occurring in order codes

* Use regex to match against blacklist

* Prevent some words from occurring in voucher codes

* Rename blacklist to banlist
2019-10-08 14:28:51 +02:00
Raphael Michel
9f7d5156cc Refs #1430 -- Fix untranslated string 2019-10-08 12:29:10 +02:00
Martin Gross
143fe6c1a6 Fix #1430 - Fix fieldname-filter for BaseInvoiceNameForm 2019-10-07 17:48:18 +02:00
Raphael Michel
1e3ccc4449 Merge pull request #1431 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-10-07 12:16:32 +02:00
Raphael Michel
d09000b716 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3210 of 3210 strings)

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

powered by weblate
2019-10-07 10:14:52 +00:00
Raphael Michel
fc4572767e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3210 of 3210 strings)

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

powered by weblate
2019-10-07 10:14:51 +00:00
Raphael Michel
eeedd9a9c2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-07 11:49:10 +02:00
Raphael Michel
1d0c148170 Fix #467 -- Pluggable email placeholders (#1429)
* Fix #467 -- Pluggable email placeholders

* Previews

* Polishing

* Fix tests

* Add missing doc file
2019-10-07 11:48:25 +02:00
Raphael Michel
cb37e7435d Use a different-colored favicon in development mode 2019-10-07 09:03:46 +02:00
Raphael Michel
749ddbf21c Allow topbar navigation subitems not to have an URL 2019-10-06 17:02:53 +02:00
Raphael Michel
9f6634025f Update from Weblate (#1425)
Update from Weblate
2019-10-06 11:56:32 +02:00
Chris Spy
ad039ae08e Translated on translate.pretix.eu (Greek)
Currently translated at 96.1% (99 of 103 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Chris Spy
eeaaca574d Translated on translate.pretix.eu (Greek)
Currently translated at 95.5% (3076 of 3220 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Fabian Rodriguez
bef15ec442 Translated on translate.pretix.eu (French)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Raphael Michel
447a8b0a8c Add missing mark_safe call 2019-10-06 11:53:37 +02:00
Sohalt
05b4d954d9 Make voucher code in notification clickable (#1423)
* Make voucher code in notification clickable

* Move html out of translated string
2019-10-06 11:52:34 +02:00
Felix Rindt
515d8c4899 remove .csv from default filename in List Exporter (#1428) 2019-10-06 11:51:24 +02:00
Raphael Michel
82497cfb89 Lazy-load widgets on global dashboard 2019-10-06 11:46:42 +02:00
Raphael Michel
4ade9d39cd Add "back" parameter to logout view 2019-10-06 11:35:29 +02:00
Raphael Michel
4360d5652b ALlow to pre-select organizer for event creation 2019-10-06 11:35:29 +02:00
Raphael Michel
9fca3188b2 Device and team creation: List events ordered and with date 2019-10-04 17:28:48 +02:00
Raphael Michel
0ed48fac7f Add pagination to event list on organizer detail page 2019-10-04 17:28:48 +02:00
Raphael Michel
27a32173e6 Move more code into change_payment_provider 2019-10-04 17:28:48 +02:00
Martin Gross
81b6188777 New ApplePay domain association file for Stripe 2019-10-02 21:31:40 +02:00
Raphael Michel
9e85d3c94c PDF editor: Catch ValueError during float conversion 2019-09-30 14:37:09 +02:00
Raphael Michel
4e58ba7594 money_filter: Idempotency on empty strings
PRETIXEU-1EH
2019-09-30 14:37:09 +02:00
Raphael Michel
248493dbf2 Stripe plugin: Fix AttributeError 2019-09-29 18:32:19 +02:00
Raphael Michel
f1bd240096 Update from Weblate (#1417)
Update from Weblate
2019-09-29 14:05:15 +02:00
Serge Bazanski
0b673dc68c Translated on translate.pretix.eu (Polish)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Serge Bazanski
b2a7fe13da Translated on translate.pretix.eu (Polish)
Currently translated at 14.7% (474 of 3220 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Martin Gross
d28f735fca Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Jonathan Oberländer
7a3b450bc6 Added translation on translate.pretix.eu (Romanian) 2019-09-27 07:11:22 +00:00
Raphael Michel
f1ec129c0a Fix ZeroDivisionError 2019-09-27 09:11:03 +02:00
Raphael Michel
ce6e46dfd2 Fix performance of check-in list API list 2019-09-26 15:18:53 +02:00
Martin Gross
f296f262e6 Properly indent handling for non-addons 2019-09-23 11:12:50 +02:00
Martin Gross
7f8d290ae1 Add-Ons inhert question-answers from parent item if necessary 2019-09-23 10:47:05 +02:00
Raphael Michel
ca0c0f4ae3 Revert to multipart/related 2019-09-20 11:22:11 +02:00
Sohalt
ac1e69f2d8 Install pretix into system-site-packages in Docker image (#1410) 2019-09-20 09:43:00 +02:00
Raphael Michel
6338cc69ae docs: fix BaseDataShredder class documentation (#1412)
The BaseDataShredder documentation contained two references to BaseInvoiceRenderer, probably due to a minor copy/paste mistake
2019-09-20 09:41:32 +02:00
Felix Rindt
39eaf3ad6a Code style improvements (#1411)
* docstring corrections

* move omit_hyphen formfield
2019-09-20 09:34:24 +02:00
Raphael Michel
76e75bef65 Fix broken organizer settings test 2019-09-19 18:33:30 +02:00
Raphael Michel
a39822aedc Use transaction aware task for regenerate_css 2019-09-19 18:17:43 +02:00
Raphael Michel
73d5a2cec0 Revert "Make all EventTasks transaction-aware"
This reverts commit 3f7807d242.
2019-09-19 18:15:47 +02:00
Raphael Michel
9f668e5fd6 Fix crash in OrderEmailHistory for unknown orders 2019-09-19 18:04:31 +02:00
Raphael Michel
1b92a891d7 Fix issues with context providers in error pages 2019-09-19 18:03:35 +02:00
Raphael Michel
827925e3c9 Fix bug in 3f7807d24 2019-09-19 18:03:28 +02:00
Johannes Lauinger
c0be574974 docs: fix BaseDataShredder class documentation
The BaseDataShredder documentation contained two references to BaseInvoiceRenderer, probably due to a minor copy/paste mistake
2019-09-19 17:57:07 +02:00
Raphael Michel
738413e8fd Allow to copy categories and quotas 2019-09-19 16:59:25 +02:00
Raphael Michel
2c6125adeb Allow to copy vouchers 2019-09-19 16:33:59 +02:00
Raphael Michel
3f7807d242 Make all EventTasks transaction-aware 2019-09-19 16:23:40 +02:00
Raphael Michel
59b7f0a03d Fix a new bug in Stripe webhooks PRETIXEU-64 2019-09-19 10:36:33 +02:00
Raphael Michel
b7dea16db3 Promote danish to an inofficial language 2019-09-19 10:13:34 +02:00
Raphael Michel
3a81706aeb Fix KeyError in bank import 2019-09-17 16:48:41 +02:00
Raphael Michel
ad3369b059 Fix issue on old-style webhook URLs (#1385)
* Fix issue on old-style webhook URLs

* Fix test issue
2019-09-17 13:59:57 +02:00
Martin Gross
ab0709558d Fix #1389: PaymentIntents are 'processing' and not 'pending' 2019-09-17 13:03:22 +02:00
Raphael Michel
e2e64ac01d Event selector: Prevent race conditions 2019-09-17 10:08:58 +02:00
Martin Gross
cf14dcf889 Add Subevent-Filter for Voucher-Tags (#1407)
* Add Subevent-Filter for Voucher-Tags

* Filter Subevent Voucher-Tags with proper Filter

* Apply filter before annotating totals and usage
2019-09-16 14:08:23 +02:00
Raphael Michel
a884a25d2b Update from Weblate (#1403)
Update from Weblate
2019-09-16 13:42:41 +02:00
Gianmarco Palumbo
8493778028 Translated on translate.pretix.eu (Italian)
Currently translated at 44.7% (46 of 103 strings)

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

powered by weblate
2019-09-13 18:00:10 +00:00
Martin Gross
36a276c360 Translated on translate.pretix.eu (French)
Currently translated at 72.5% (2335 of 3220 strings)

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

powered by weblate
2019-09-13 18:00:09 +00:00
Maarten van den Berg
795c423d73 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-12 22:00:12 +00:00
Maarten van den Berg
3ce9ec79f0 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-12 22:00:10 +00:00
Translators EN IT Team
debc5e255b Translated on translate.pretix.eu (Italian)
Currently translated at 4.7% (151 of 3220 strings)

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

powered by weblate
2019-09-12 00:00:09 +00:00
Raphael Michel
145aa0d7fd Merge pull request #1397 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-11 08:58:42 +02:00
Raphael Michel
3997bdd098 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:58:06 +00:00
Raphael Michel
a51a905512 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:58:06 +00:00
Maarten van den Berg
d3ac9e8880 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:55:48 +00:00
Maarten van den Berg
07402c9ea0 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:55:47 +00:00
Raphael Michel
5f81bf55ca Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-11 08:55:36 +02:00
Raphael Michel
0120a5a930 Clarify cancellation description 2019-09-11 08:55:03 +02:00
Raphael Michel
ba555f956e Fix #1395 -- Incorrect data in tax list exporter if an order contains multiple tax rates 2019-09-10 14:41:49 +02:00
Raphael Michel
5e3876ddde Tax exporters: Use lazy gettext calls 2019-09-10 14:40:05 +02:00
Raphael Michel
f5719687aa Use lazy translation for exporter name 2019-09-10 13:13:15 +02:00
Raphael Michel
18449efcc7 Fix #1394 -- Forcefully update defusedcsv 2019-09-10 12:10:41 +02:00
Raphael Michel
3bb20d943a Fix incorrect condition in template 2019-09-10 11:58:02 +02:00
Raphael Michel
586e544fce Add "resend link" option to attendees 2019-09-10 11:44:59 +02:00
Raphael Michel
3a4fc69db1 Add link to ticket page to order page in backend 2019-09-10 11:34:52 +02:00
Raphael Michel
8b5d49d82f Dekodi exporter: Fix AttributeError 2019-09-10 11:34:19 +02:00
Raphael Michel
7665faa39f Bump version to 3.2.0.dev0 2019-09-10 10:54:25 +02:00
412 changed files with 137650 additions and 66244 deletions

View File

@@ -14,18 +14,18 @@ if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
fi
if [ "$1" == "style" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements.txt -r src/requirements/dev.txt
cd src
flake8 .
isort -c -rc -df .
fi
if [ "$1" == "doctests" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
cd doc
make doctest
fi
if [ "$1" == "doc-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
cd doc
make spelling
if [ -s _build/spelling/output.txt ]; then
@@ -33,26 +33,26 @@ if [ "$1" == "doc-spelling" ]; then
fi
fi
if [ "$1" == "translation-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
cd src
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 3 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
coverage run -m py.test --reruns 5 tests && codecov
fi
if [ "$1" == "plugins" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python setup.py develop
make all compress

View File

@@ -3,7 +3,7 @@ Contributing to pretix
Hey there and welcome to pretix!
We've got an contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
We've got a contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
together with notes on the [development setup](https://docs.pretix.eu/en/latest/development/setup.html).
Please note that we have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)

View File

@@ -57,6 +57,8 @@ COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
RUN cd /pretix/src && pip3 install .
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
cd /pretix/src && \

View File

@@ -57,6 +57,9 @@ Example::
A comma-separated list of plugins that are not available even though they are installed.
Defaults to an empty string.
``auth_backends``
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.

View File

@@ -276,7 +276,7 @@ choice)::
Then, go to that directory and build the image::
$ docker build -t mypretix
$ docker build . -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.

View File

@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser

View File

@@ -1,3 +1,5 @@
.. spelling:: checkin
Check-in lists
==============
@@ -27,6 +29,7 @@ subevent integer ID of the date
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -41,6 +44,10 @@ include_pending boolean If ``true``, th
The ``include_pending`` field has been added.
.. versionchanged:: 3.2
The ``auto_checkin_sales_channels`` field has been added.
Endpoints
---------
@@ -81,7 +88,10 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
]
}
@@ -122,7 +132,10 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -215,7 +228,10 @@ Endpoints
"name": "VIP entry",
"all_products": false,
"limit_products": [1, 2],
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
**Example response**:
@@ -234,7 +250,10 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
@@ -283,7 +302,10 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer to modify
@@ -342,6 +364,11 @@ Order position endpoints
``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
@@ -400,7 +427,8 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [
@@ -510,7 +538,8 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [

View File

@@ -1,3 +1,9 @@
.. spelling::
geo
lat
lon
Events
======
@@ -25,6 +31,8 @@ is_public boolean If ``true``, th
presale_start datetime The date at which the ticket shop opens (or ``null``)
presale_end datetime The date at which the ticket shop closes (or ``null``)
location multi-lingual string The event location (or ``null``)
geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``)
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data object Values set for organizer-specific meta data parameters.
@@ -62,9 +70,17 @@ seat_category_mapping object An object mappi
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.
Endpoints
---------
.. versionchanged:: 3.3
The events resource can now be filtered by meta data attributes.
.. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -105,6 +121,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"meta_data": {},
"seating_plan": null,
@@ -129,6 +147,10 @@ Endpoints
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
Default: ``slug``.
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of events will
only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
set. Please note that this filter will respect default values set on organizer level.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -169,6 +191,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
@@ -220,6 +244,8 @@ Endpoints
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"meta_data": {},
"plugins": [
@@ -249,6 +275,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
@@ -268,11 +296,11 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
their value will be copied from the existing event.
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
value. Otherwise their value will be copied from the existing event.
Please note that you can only copy from events under the same organizer.
@@ -300,6 +328,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
@@ -331,6 +361,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
@@ -394,6 +426,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},

View File

@@ -0,0 +1,229 @@
.. _`rest-giftcards`:
Gift cards
==========
Resource description
--------------------
The gift card resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the gift card
secret string Gift card code (can not be modified later)
value money (string) Current gift card value
currency string Currency of the value (can not be modified later)
testmode boolean Whether this is a test gift card
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/giftcards/
Returns a list of all gift cards issued by a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "13.37"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/
Returns information on one gift card, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the gift card to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/giftcards/
Creates a new gift card
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"value": "13.37"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to create a gift card for
:statuscode 201: no error
:statuscode 400: The gift card could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/giftcards/(id)/
Update a gift card. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id``, ``secret``, ``testmode``, and ``currency`` fields. Be
careful when modifying the ``value`` field to avoid race conditions. We recommend to use the ``transact`` method
described below.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"value": "14.00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"value": "14.00"
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify
:statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:post:: /api/v1/organizers/(organizer)/giftcards/(id)/transact/
Atomically change the value of a gift card. A positive amount will increase the value of the gift card,
a negative amount will decrease it.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"value": "2.00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "15.37"
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify
:statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.

View File

@@ -21,6 +21,7 @@ Resources and endpoints
vouchers
checkinlists
waitinglist
giftcards
carts
webhooks
seatingplans

View File

@@ -28,6 +28,7 @@ payment_provider_text string Text to be prin
payment information
footer_text string Text to be printed in the page footer area
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)
├ gross_value money (string) Price including taxes
├ tax_value money (string) Tax amount included
@@ -63,6 +64,11 @@ internal_reference string Customer's refe
The attribute ``internal_reference`` has been added.
.. versionchanged:: 3.4
The attribute ``lines.number`` has been added.
Endpoints
---------
@@ -107,6 +113,7 @@ Endpoints
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
"position": 1,
"description": "Budget Ticket",
"gross_value": "23.00",
"tax_value": "0.00",
@@ -171,6 +178,7 @@ Endpoints
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
"position": 1,
"description": "Budget Ticket",
"gross_value": "23.00",
"tax_value": "0.00",

View File

@@ -77,6 +77,7 @@ generate_tickets boolean If ``false``, t
rules apply.
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
product when it is sold out.
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used.
has_variations boolean Shows whether or not this item has variations.
@@ -206,6 +207,7 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -300,6 +302,7 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -375,6 +378,7 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -437,6 +441,7 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -531,6 +536,7 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,

View File

@@ -131,16 +131,16 @@ last_modified datetime Last modificati
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4:
.. 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:
.. versionchanged:: 2.5
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1:
.. 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.
@@ -175,7 +175,8 @@ subevent integer ID of the date
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
datetime datetime Time of check-in
datetime datetime Time of check-in
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
@@ -214,6 +215,15 @@ pdf_data object Data object req
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
:ref:`order-position-ticket-download` for details.
.. _order-payment-resource:
Order payment resource
@@ -365,7 +375,8 @@ List of all orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -512,7 +523,8 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -762,6 +774,8 @@ Creating orders
* does not support file upload questions
* does not support redeeming gift cards
You can supply the following fields of the resource:
* ``code`` (optional)
@@ -778,8 +792,9 @@ Creating orders
* ``locale``
* ``sales_channel``
* ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid.
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
zero, otherwise it is required.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
@@ -830,6 +845,14 @@ Creating orders
* ``description``
* ``internal_type``
* ``tax_rule``
* ``_treat_value_as_percentage`` (Optional convenience flag. If set to ``true``, your ``value`` parameter will
be treated as a percentage and the fee will be calculated using that percentage and the sum of all product
prices. Note that this will not include other fees and is calculated once during order generation and will not
be respected automatically when the order changes later.)
* ``_split_taxes_like_products`` (Optional convenience flag. If set to ``true``, your ``tax_rule`` will be ignored
and the fee will be taxed like the products in the order. If the products have multiple tax rates, multiple fees
will be generated with weights adjusted to the net price of the products. Note that this will be calculated once
during order generation and is not respected automatically when the order changes later.)
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
@@ -1286,6 +1309,11 @@ List of all order positions
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.
.. note:: Individually canceled order positions are currently not visible via the API at all.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
@@ -1337,7 +1365,8 @@ List of all order positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1438,7 +1467,8 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1466,6 +1496,8 @@ Fetching individual positions
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
.. _`order-position-ticket-download`:
Order position ticket download
------------------------------
@@ -1475,6 +1507,11 @@ Order position ticket download
Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details
response contains a list of output options for this particular order position.
Be aware that the output does not have to be a file, but can also be a regular HTTP response with a ``Content-Type``
set to ``text/uri-list``. In this case, the user is expected to navigate to that URL in order to access their ticket.
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
in a webbrowser and leave it up to the user to handle the result.
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
configuration downloads might be only unavailable for add-on products or non-admission products.
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status

View File

@@ -31,6 +31,7 @@ type string The expected ty
* ``H`` time
* ``W`` date and time
* ``CC`` country code (ISO 3666-1 alpha-2)
* ``TEL`` telephone number
required boolean If ``true``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.

View File

@@ -1,3 +1,9 @@
.. spelling::
geo
lat
lon
.. _rest-subevents:
Event series dates / Sub-events
@@ -28,6 +34,8 @@ date_admission datetime The sub-event's
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``)
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``)
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
@@ -62,9 +70,17 @@ seat_category_mapping object An object mappi
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.
Endpoints
---------
.. versionchanged:: 3.3
The sub-events resource can now be filtered by meta data attributes.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Returns a list of all sub-events of an event.
@@ -104,6 +120,8 @@ Endpoints
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"geo_lat": null,
"geo_lon": null,
"item_price_overrides": [
{
"item": 2,
@@ -123,6 +141,11 @@ Endpoints
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the main event
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of sub-events
will only contain the sub-events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
have no value set. Please note that this filter will respect default values set on
organizer or event level.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
@@ -152,6 +175,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -184,6 +209,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -237,6 +264,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -303,6 +332,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -389,6 +420,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [

View File

@@ -38,6 +38,7 @@ quota integer An ID of a quot
attached either to a specific product or to all
products within one quota or it can be available
for all items without restriction.
seat string ``seat_guid`` attribute of a specific seat (or ``null``)
tag string A string that is used for grouping vouchers
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
@@ -53,6 +54,10 @@ show_hidden_items boolean Only if set to
The attribute ``show_hidden_items`` has been added.
.. versionchanged:: 3.4
The attribute ``seat`` has been added.
Endpoints
---------
@@ -96,7 +101,8 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"seat": null,
"subevent": null,
}
]
}
@@ -162,6 +168,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
}
@@ -225,6 +232,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
}
@@ -352,6 +360,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
}

12
doc/contents.rst Normal file
View File

@@ -0,0 +1,12 @@
Table of contents
=================
.. toctree::
:maxdepth: 3
user/index
admin/index
api/index
development/index
plugins/index

View File

@@ -0,0 +1,70 @@
.. highlight:: python
:linenothreshold: 5
Pluggable authentication backends
=================================
Plugins can supply additional authentication backends. This is mainly useful in self-hosted installations
and allows you to use company-wide login mechanisms such as LDAP or OAuth for accessing pretix' backend.
Every authentication backend contains an implementation of the interface defined in ``pretix.base.auth.BaseAuthBackend``
(see below). Note that pretix authentication backends work differently than plain Django authentication backends.
Basically, three pre-defined flows are supported:
* Authentication mechanisms that rely on a **set of input parameters**, e.g. a username and a password. These can be
implemented by supplying the ``login_form_fields`` property and a ``form_authenticate`` method.
* Authentication mechanisms that rely on **external sessions**, e.g. a cookie or a proxy HTTP header. These can be
implemented by supplying a ``request_authenticate`` method.
* Authentication mechanisms that rely on **redirection**, e.g. to an OAuth provider. These can be implemented by
supplying a ``authentication_url`` method and implementing a custom return view.
Authentication backends are *not* collected through a signal. Instead, they must explicitly be set through the
``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
In each of these methods (``form_authenticate``, ``request_authenticate`` or your custom view) you are supposed to
either get an existing :py:class:`pretix.base.models.User` object from the database or create a new one. There are a
few rules you need to follow:
* You **MUST** only return users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
* You **MUST** create new users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
* Every user object **MUST** have an email address. Email addresses are globally unique. If the email address is
already registered to a user who signs in through a different backend, you **SHOULD** refuse the login.
The backend interface
---------------------
.. class:: pretix.base.auth.BaseAuthBackend
The central object of each backend is the subclass of ``BaseAuthBackend``.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. autoattribute:: login_form_fields
.. autoattribute:: visible
.. automethod:: form_authenticate
.. automethod:: request_authenticate
.. automethod:: authentication_url
Logging users in
----------------
If you return a user from ``form_authenticate`` or ``request_authenticate``, the system will handle everything else
for you correctly. However, if you use a redirection method and build a custom view to verify the login, we strongly
recommend that you use the following utility method to correctly set session values and enforce two-factor
authentication (if activated):
.. autofunction:: pretix.control.views.auth.process_login

View File

@@ -20,13 +20,13 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, 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
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, 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, item_description
.. automodule:: pretix.presale.signals
@@ -49,8 +49,8 @@ Backend
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, item_formsets
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
item_formsets, order_search_filter_q
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
@@ -78,3 +78,6 @@ Ticket designs
.. automodule:: pretix.base.signals
:members: layout_text_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: override_layout

View File

@@ -12,8 +12,10 @@ Contents:
payment
payment_2.0
email
placeholder
invoice
shredder
customview
auth
general
quality

View File

@@ -62,6 +62,8 @@ The provider class
.. autoattribute:: is_enabled
.. autoattribute:: priority
.. autoattribute:: settings_form_fields
.. automethod:: settings_form_clean
@@ -108,10 +110,14 @@ The provider class
.. automethod:: execute_refund
.. automethod:: refund_control_render
.. automethod:: api_payment_details
.. automethod:: shred_payment_info
.. automethod:: cancel_payment
.. autoattribute:: is_implicit
.. autoattribute:: is_meta

View File

@@ -0,0 +1,79 @@
.. highlight:: python
:linenothreshold: 5
Writing an e-mail placeholder plugin
====================================
An email placeholder is a dynamic value that pretix users can use in their email templates.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
from django.dispatch import receiver
from pretix.base.signals import register_mail_placeholders
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense in every email, and placeholders usually depend some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in an email if all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
* ``order``
* ``position``
* ``waiting_list_entry``
* ``invoice_address``
* ``payment``
There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: required_context
This is an abstract attribute, you **must** override this!
.. automethod:: render
This is an abstract method, you **must** implement this!
.. automethod:: render_sample
This is an abstract method, you **must** implement this!
Helper class for simple placeholders
------------------------------------
pretix ships with a helper class that makes it easy to provide placeholders based on simple
functions::
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)

View File

@@ -35,9 +35,9 @@ The shredder class
.. class:: pretix.base.shredder.BaseDataShredder
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
The central object of each data shredder is the subclass of ``BaseDataShredder``.
.. py:attribute:: BaseInvoiceRenderer.event
.. py:attribute:: BaseDataShredder.event
The default constructor sets this property to the event we are currently
working for.

View File

@@ -69,3 +69,9 @@ The output class
.. automethod:: generate_order
.. autoattribute:: download_button_text
.. autoattribute:: download_button_icon
.. autoattribute:: preview_allowed
.. autoattribute:: javascript_required

View File

@@ -1,12 +1 @@
Table of contents
=================
.. toctree::
:maxdepth: 3
user/index
admin/index
api/index
development/index
plugins/index
.. include:: contents.rst

View File

@@ -31,6 +31,7 @@ deprovision
discoverable
django
dockerfile
downloadable
durations
eu
filename
@@ -41,6 +42,7 @@ formsets
frontend
frontpage
gettext
giftcard
gunicorn
guid
hardcoded
@@ -55,6 +57,7 @@ Jimdo
libpretixprint
libsass
linters
login
memcached
metadata
middleware
@@ -76,6 +79,7 @@ overpayment
param
passphrase
percental
pluggable
positionid
pre
prepend
@@ -137,6 +141,7 @@ username
url
versa
versioning
viewable
viewset
viewsets
waitinglist

View File

@@ -0,0 +1,71 @@
.. spelling::
Warengutschein
Wertgutschein
Gift cards
==========
Gift cards, also known as "gift coupons" or "gift certificates" are a mechanism that allows you to sell tokens that
can later be used to pay for tickets.
Gift cards are very different feature than **vouchers**. The difference is:
* Vouchers can be used to give a discount. When a voucher is used, the price of a ticket is reduced by the configured
discount and sold at a lower price. They therefore reduce both revenue as well as taxes. Vouchers (in pretix) are
always specific to a certain product in an order. Vouchers are usually not sold but given out as part of a
marketing campaign or to specific groups of people. Vouchers in pretix are bound to a specific event.
* Gift cards are not a discount, but rather a means of payment. If you buy a €20 ticket with a €10 gift card, it is
still a €20 ticket and will still count towards your revenue with €20. Gift cards are usually bought for the money
that they are worth. Gift cards in pretix can be used across events (and even organizers).
Selling gift cards
------------------
Selling gift cards works like selling every other type of product in pretix: Create a new product, then head to
"Additional settings" and select the option "This product is a gift card". Whenever someone buys this product and
pays for it, a new gift card will be created.
In this case, the gift card code corresponds to the "ticket secret" in the PDF ticket. Therefore, if selling gift cards,
you can use ticket downloads just as with normal tickets and use our ticket editor to create beautiful gift certificates
people can give to their loved ones.
Of course, you can use pretix' flexible options to modify your product. For example, you can configure that the customer
can freely choose the price of the gift card.
.. note::
pretix currently does not support charging sales tax or VAT when selling gift cards, but instead charges VAT on
the full price when the gift card is redeemed. This is the correct behavior in Germany and some other countries for
gift cards which are not bound to a very specific service ("Warengutschein"), but instead to a monetary amount
("Wertgutschein").
.. note::
The ticket PDF will not contain the correct gift card code before the order has been paid, so we recommend not
selling gift cards in events where tickets are issued before payments arrive.
Accepting gift cards
--------------------
All your events have have the payment provider "Gift card" enabled by default, but it will only show up in the ticket
shop once the very first gift card has been issued on your organizer account. Of course, you can turn off gift card
payments if you do not want them for a specific event.
If gift card payments are enabled, buyers will be able to select "Gift card" as a payment method during checkout. If
a gift card with a value less than the order total is used, the buyer will be asked to select a second payment method
for the remaining payment. If a gift card with a value greater than the order total is used, the surplus amount
remains on the gift card and can be used in a different purchase.
If it possible to accept gift cards across organizer accounts. To do so, you need to have access to both organizer
accounts. Then, you will see a configuration section at the bottom of the "Gift cards" page of your organizer settings
where you can specify which gift cards should be accepted.
Manually issuing or using gift cards
------------------------------------
Of course, you can also issue or redeem gift cards manually through our backend using the "Gift cards" menu item in your
organizer profile or using our API. These gift cards will be tracked by pretix, but do not correspond to any purchase
within pretix. You will therefore need to account for them in your books separately.

View File

@@ -206,6 +206,21 @@ If you want, you can suppress us loading the widget and/or modify the user data
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
Waiting for the widget to load
------------------------------
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
e.g. from an event list to an event detail view::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
window.PretixWidget.addLoadListener(function () {
console.log("Widget has loaded!");
});
}
</script>
Passing user data to the widget
-------------------------------
@@ -241,6 +256,11 @@ This works for the pretix Button as well. Currently, the following attributes ar
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
country code.
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
on this for authentication.
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:

File diff suppressed because one or more lines are too long

View File

@@ -12,5 +12,6 @@ wanting to use pretix to sell tickets.
events/settings
events/structureguide
events/widget
events/giftcards
faq
markdown
markdown

View File

@@ -1 +1 @@
__version__ = "3.1.0"
__version__ = "3.4.0"

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.1 on 2019-10-28 15:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0004_auto_20190405_1048'),
]
operations = [
migrations.AlterField(
model_name='oauthgrant',
name='redirect_uri',
field=models.CharField(max_length=2500),
),
]

View File

@@ -43,6 +43,7 @@ class OAuthGrant(AbstractGrant):
OAuthApplication, on_delete=models.CASCADE
)
organizers = models.ManyToManyField('pretixbase.Organizer')
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
class OAuthAccessToken(AbstractAccessToken):

View File

@@ -3,6 +3,7 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import CheckinList
@@ -13,7 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending')
'include_pending', 'auto_checkin_sales_channels')
def validate(self, data):
data = super().validate(data)
@@ -35,4 +36,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
for channel in full_data.get('auto_checkin_sales_channels') or []:
if channel not in get_all_sales_channels():
raise ValidationError(_('Unknown sales channel.'))
return data

View File

@@ -70,7 +70,7 @@ class EventSerializer(I18nAwareModelSerializer):
model = Event
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping')
def validate(self, data):
@@ -239,6 +239,7 @@ class CloneEventSerializer(EventSerializer):
plugins = validated_data.pop('plugins', None)
is_public = validated_data.pop('is_public', None)
testmode = validated_data.pop('testmode', None)
has_subevents = validated_data.pop('has_subevents', None)
new_event = super().create(validated_data)
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
@@ -250,6 +251,8 @@ class CloneEventSerializer(EventSerializer):
new_event.is_public = is_public
if testmode is not None:
new_event.testmode = testmode
if has_subevents is not None:
new_event.has_subevents = has_subevents
new_event.save()
return new_event
@@ -277,8 +280,9 @@ class SubEventSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
'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')
def validate(self, data):
data = super().validate(data)

View File

@@ -119,12 +119,9 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
return {"has_variations": self.kwargs['has_variations']}
def validate(self, data):
data = super().validate(data)
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
@@ -134,6 +131,17 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
if data.get('issue_giftcard'):
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
raise ValidationError(
_("Gift card products should not be associated with non-zero tax rates since sales tax will be "
"applied when the gift card is redeemed.")
)
if data.get('admission'):
raise ValidationError(_(
"Gift card products should not be admission products at the same time."
))
return data
def validate_category(self, value):
@@ -249,6 +257,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
dep = full_data.get('dependency_question')
if dep:
if dep.ask_during_checkin:
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
seen_ids = {self.instance.pk} if self.instance else set()
while dep:
if dep.pk in seen_ids:
@@ -256,6 +267,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
seen_ids.add(dep.pk)
dep = dep.dependency_question
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be asked during check-in.'))
Question.clean_items(event, full_data.get('items'))
return data

View File

@@ -1,5 +1,5 @@
import json
from collections import Counter
from collections import Counter, defaultdict
from decimal import Decimal
import pycountry
@@ -14,10 +14,11 @@ from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, Voucher,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -114,7 +115,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list')
fields = ('datetime', 'list', 'auto_checked_in')
class OrderDownloadsField(serializers.Field):
@@ -477,9 +478,13 @@ class AnswerCreateSerializer(I18nAwareModelSerializer):
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
_treat_value_as_percentage = serializers.BooleanField(default=False, required=False)
_split_taxes_like_products = serializers.BooleanField(default=False, required=False)
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule',
'_treat_value_as_percentage', '_split_taxes_like_products')
def validate_tax_rule(self, tr):
if tr and tr.event != self.context['event']:
@@ -675,6 +680,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(errs)
return data
def validate_testmode(self, testmode):
if 'sales_channel' in self.initial_data:
try:
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
if testmode and not sales_channel.testmode_supported:
raise ValidationError('This sales channel does not provide support for testmode.')
except KeyError:
# We do not need to raise a ValidationError here, since there is another check to validate the
# sales_channel
pass
return testmode
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
@@ -849,15 +868,53 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for cp in delete_cps:
cp.delete()
order.total = sum([p.price for p in order.positions.all()])
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
if is_percentage:
fee_data['value'] = round_decimal(order.total * (fee_data['value'] / Decimal('100.00')),
self.context['event'].currency)
is_split_taxes = fee_data.pop('_split_taxes_like_products', False)
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
if is_split_taxes:
d = defaultdict(lambda: Decimal('0.00'))
trz = TaxRule.zero()
for p in pos_map.values():
tr = p.tax_rule
d[tr] += p.price - p.tax_value
base_values = sorted([tuple(t) for t in d.items()], key=lambda t: (t[0] or trz).rate)
sum_base = sum(t[1] for t in base_values)
fee_values = [(t[0], round_decimal(fee_data['value'] * t[1] / sum_base, self.context['event'].currency))
for t in base_values]
sum_fee = sum(t[1] for t in fee_values)
# If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
# authorities
if sum_fee > fee_data['value']:
fee_values[0] = (fee_values[0][0], fee_values[0][1] + (fee_data['value'] - sum_fee))
elif sum_fee < fee_data['value']:
fee_values[-1] = (fee_values[-1][0], fee_values[-1][1] + (fee_data['value'] - sum_fee))
for tr, val in fee_values:
fee_data['tax_rule'] = tr
fee_data['value'] = val
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
else:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
order.total += sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
@@ -888,10 +945,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
return order
class LinePositionField(serializers.IntegerField):
"""
Internally, the position field is stored starting at 0, but for the API, starting at 1 makes it
more consistent with other models
"""
def to_representation(self, value):
return super().to_representation(value) + 1
def to_internal_value(self, data):
return super().to_internal_value(data) - 1
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
position = LinePositionField(read_only=True)
class Meta:
model = InvoiceLine
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
class InvoiceSerializer(I18nAwareModelSerializer):

View File

@@ -1,6 +1,11 @@
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.models import Organizer, SeatingPlan
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.base.models.seating import SeatingPlanLayoutValidator
@@ -18,3 +23,27 @@ class SeatingPlanSerializer(I18nAwareModelSerializer):
class Meta:
model = SeatingPlan
fields = ('id', 'name', 'layout')
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=10, decimal_places=2)
def validate(self, data):
data = super().validate(data)
s = data['secret']
qs = GiftCard.objects.filter(
secret=s
).filter(
Q(issuer=self.context["organizer"]) | Q(issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(
{'secret': _('A gift card with the same secret already exists in your or an affiliated organizer account.')}
)
return data
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')

View File

@@ -2,15 +2,23 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Voucher
from pretix.base.models import Seat, Voucher
class VoucherListSerializer(serializers.ListSerializer):
def create(self, validated_data):
codes = set()
seats = set()
errs = []
err = False
for voucher_data in validated_data:
if voucher_data.get('seat') and (voucher_data.get('seat'), voucher_data.get('subevent')) in seats:
err = True
errs.append({'code': ['Duplicate seat ID in request.']})
continue
else:
seats.add((voucher_data.get('seat'), voucher_data.get('subevent')))
if voucher_data['code'] in codes:
err = True
errs.append({'code': ['Duplicate voucher code in request.']})
@@ -22,12 +30,19 @@ class VoucherListSerializer(serializers.ListSerializer):
return super().create(validated_data)
class SeatGuidField(serializers.CharField):
def to_representation(self, val: Seat):
return val.seat_guid
class VoucherSerializer(I18nAwareModelSerializer):
seat = SeatGuidField(allow_null=True, required=False)
class Meta:
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items')
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer
@@ -39,7 +54,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
Voucher.clean_item_properties(
full_data, self.context.get('event'),
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
full_data.get('quota'), full_data.get('item'), full_data.get('variation'),
block_quota=full_data.get('block_quota')
)
Voucher.clean_subevent(
full_data, self.context.get('event')
@@ -61,4 +77,10 @@ class VoucherSerializer(I18nAwareModelSerializer):
)
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
if full_data.get('seat'):
data['seat'] = Voucher.clean_seat_id(
full_data, full_data.get('item'), full_data.get('quota'), self.context.get('event'),
self.instance.pk if self.instance else None
)
return data

View File

@@ -19,6 +19,7 @@ orga_router.register(r'events', event.EventViewSet)
orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)

View File

@@ -44,7 +44,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
qs = self.request.event.checkin_lists.prefetch_related(
'limit_products',
)
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
return qs
def perform_create(self, serializer):

View File

@@ -18,6 +18,7 @@ from pretix.base.models import (
)
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
from pretix.presale.views.organizer import filter_qs_by_attr
with scopes_disabled():
class EventFilter(FilterSet):
@@ -85,6 +86,8 @@ class EventViewSet(viewsets.ModelViewSet):
organizer=self.request.organizer
)
qs = filter_qs_by_attr(qs, self.request)
return qs.prefetch_related(
'meta_values', 'meta_values__property', 'seat_category_mappings'
)
@@ -241,6 +244,9 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_any_permission()
)
qs = filter_qs_by_attr(qs, self.request)
return qs.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
)

View File

@@ -62,7 +62,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['has_variations'] = self.request.data.get('has_variations')
return ctx
def perform_update(self, serializer):

View File

@@ -6,7 +6,7 @@ import pytz
from django.db import transaction
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
@@ -48,7 +48,7 @@ from pretix.base.services.orders import (
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate
from pretix.base.signals import (
order_modified, order_placed, register_ticket_outputs,
order_modified, order_paid, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
@@ -148,12 +148,16 @@ class OrderViewSet(viewsets.ModelViewSet):
generate.apply_async(args=('order', order.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
if ct.type == 'text/uri-list':
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -169,9 +173,26 @@ class OrderViewSet(viewsets.ModelViewSet):
amount=ps
)
except OrderPayment.DoesNotExist:
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)):
try:
with transaction.atomic():
p.payment_provider.cancel_payment(p)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': p.local_id,
'provider': p.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
except PaymentException as e:
order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': p.local_id,
'provider': p.provider,
'error': str(e)
},
user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth
)
p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider='manual',
@@ -441,48 +462,52 @@ class OrderViewSet(viewsets.ModelViewSet):
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
invoice = None
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
with language(order.locale):
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)
if send_mail:
payment = order.payments.last()
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
not order.require_approval and payment.provider == "free"
)
if free_flow:
email_template = request.event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
invoice = None
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if send_mail:
payment = order.payments.last()
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
not order.require_approval and payment.provider == "free"
)
if free_flow:
email_template = request.event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
if self.request.event.settings.mail_send_order_paid_attendee:
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
payment._send_paid_mail_attendee(p, None)
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
if self.request.event.settings.mail_send_order_paid_attendee:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
payment._send_paid_mail_attendee(p, None)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@@ -759,12 +784,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
if ct.type == 'text/uri-list':
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
def perform_destroy(self, instance):
try:
@@ -886,13 +915,16 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id,
'provider': payment.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
try:
with transaction.atomic():
payment.payment_provider.cancel_payment(payment)
payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id,
'provider': payment.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
except PaymentException as e:
return Response({'detail': 'External error: {}'.format(str(e))},
status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)

View File

@@ -1,11 +1,14 @@
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
from django.db import transaction
from rest_framework import filters, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import (
OrganizerSerializer, SeatingPlanSerializer,
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
)
from pretix.base.models import Organizer, SeatingPlan
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.helpers.dicts import merge_dicts
@@ -81,3 +84,66 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
data={'id': instance.pk}
)
instance.delete()
class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
def get_queryset(self):
return self.request.organizer.issued_gift_cards.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.transactions.create(value=value)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
diff = value - old_value
inst.transactions.create(value=diff)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
return inst
@action(detail=True, methods=["POST"])
@transaction.atomic()
def transact(self, request, **kwargs):
gc = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value')
)
gc.transactions.create(value=value)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value}
)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")

View File

@@ -45,7 +45,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_vouchers'
def get_queryset(self):
return self.request.event.vouchers.all()
return self.request.event.vouchers.select_related('seat').all()
def _predict_quota_check(self, data, instance):
# This method predicts if Voucher.clean_quota_needs_checking

View File

@@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
try:
from .celery_app import app as celery_app # NOQA

109
src/pretix/base/auth.py Normal file
View File

@@ -0,0 +1,109 @@
from collections import OrderedDict
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.utils.translation import gettext_lazy as _
def get_auth_backends():
backends = {}
for b in settings.PRETIX_AUTH_BACKENDS:
mod, name = b.rsplit('.', 1)
b = getattr(import_module(mod), name)()
backends[b.identifier] = b
return backends
class BaseAuthBackend:
"""
This base class defines the interface that needs to be implemented by every class that supplies
an authentication method to pretix. Please note that pretix authentication backends are different
from plain Django authentication backends! Be sure to read the documentation chapter on authentication
backends before you implement one.
"""
@property
def identifier(self):
"""
A short and unique identifier for this authentication backend.
This should only contain lowercase letters and in most cases will
be the same as your package name.
"""
raise NotImplementedError()
@property
def verbose_name(self):
"""
A human-readable name of this authentication backend.
"""
raise NotImplementedError()
@property
def visible(self):
"""
Whether or not this backend can be selected by users actively. Set this to ``False``
if you only implement ``request_authenticate``.
"""
return True
@property
def login_form_fields(self) -> dict:
"""
This property may return form fields that the user needs to fill in to log in.
"""
return {}
def form_authenticate(self, request, form_data):
"""
This method will be called after the user filled in the login form. ``request`` will contain
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
You are expected to either return a ``User`` object (if login was successful) or ``None``.
"""
return
def request_authenticate(self, request):
"""
This method will be called when the user opens the login form. If the user already has a valid session
according to your login mechanism, for example a cookie set by a different system or HTTP header set by a
reverse proxy, you can directly return a ``User`` object that will be logged in.
``request`` will contain the current request.
You are expected to either return a ``User`` object (if login was successful) or ``None``.
"""
return
def authentication_url(self, request):
"""
This method will be called to populate the URL for your authentication method's tab on the login page.
For example, if your method works through OAuth, you could return the URL of the OAuth authorization URL the
user needs to visit.
If you return ``None`` (the default), the link will point to a page that shows the form defined by
``login_form_fields``.
"""
return
class NativeAuthBackend(BaseAuthBackend):
identifier = 'native'
verbose_name = _('pretix User')
@property
def login_form_fields(self) -> dict:
"""
This property may return form fields that the user needs to fill in
to log in.
"""
d = OrderedDict([
('email', forms.EmailField(label=_("E-mail"), max_length=254,
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
])
return d
def form_authenticate(self, request, form_data):
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
if u and u.auth_backend == self.identifier:
return u

View File

@@ -0,0 +1,80 @@
import re
# banlist based on http://www.bannedwordlist.com/lists/swearWords.txt
banlist = [
"anal",
"anus",
"arse",
"ass",
"balls",
"bastard",
"bitch",
"biatch",
"bloody",
"blowjob",
"bollock",
"bollok",
"boner",
"boob",
"bugger",
"bum",
"butt",
"clitoris",
"cock",
"coon",
"crap",
"cunt",
"damn",
"dick",
"dildo",
"dyke",
"fag",
"feck",
"fellate",
"fellatio",
"felching",
"fuck",
"fudgepacker",
"flange",
"goddamn",
"hell",
"homo",
"jerk",
"jizz",
"knobend",
"labia",
"lmao",
"lmfao",
"muff",
"nigger",
"nigga",
"omg",
"penis",
"piss",
"poop",
"prick",
"pube",
"pussy",
"queer",
"scrotum",
"sex",
"shit",
"sh1t",
"slut",
"smegma",
"spunk",
"tit",
"tosser",
"turd",
"twat",
"vagina",
"wank",
"whore",
"wtf"
]
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
def banned(string):
return bool(blacklist_regex.search(string.lower()))

View File

@@ -35,6 +35,32 @@ class SalesChannel:
"""
return "circle"
@property
def testmode_supported(self) -> bool:
"""
Indication, if a saleschannels supports test mode orders
"""
return True
@property
def payment_restrictions_supported(self) -> bool:
"""
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
the event organizer cannot restrict the usage of any payment provider through the backend.
"""
return True
@property
def unlimited_items_per_order(self) -> bool:
"""
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
items defined in the event settings.
"""
return False
def get_all_sales_channels():
global _ALL_CHANNELS

View File

@@ -0,0 +1,15 @@
import sys
from django.conf import settings
def contextprocessor(request):
ctx = {
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
}
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True
elif 'runserver' in sys.argv:
ctx['development_warning'] = True
return ctx

View File

@@ -1,15 +1,23 @@
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from smtplib import SMTPResponseException
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.signals import register_html_mail_renderers
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
from pretix.base.models import Event
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
logger = logging.getLogger('pretix.base.email')
@@ -44,8 +52,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -97,7 +105,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
@@ -136,3 +144,305 @@ class ClassicMailRenderer(TemplateBasedMailRenderer):
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
def base_renderers(sender, **kwargs):
return [ClassicMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'order' in kwargs:
try:
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress()
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
ctx[v.identifier] = v.render(kwargs)
return ctx
def _placeholder_payment(order, payment):
if not payment:
return None
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
return str(payment.payment_provider.order_pending_mail_render(order, payment))
else:
return str(payment.payment_provider.order_pending_mail_render(order))
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.base.models import InvoiceAddress
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
# TODO: This used to be "date" in some placeholders, add a migration!
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret']
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalMailTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payment'], _placeholder_payment,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
lambda position_or_address: (
position_or_address.name
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name
),
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: position.attendee_name_parts.get(f, ''),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: (
position_or_address.name_parts.get(f, '')
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name_parts.get(f, '')
),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
return ph

View File

@@ -111,7 +111,7 @@ class ListExporter(BaseExporter):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export.csv'
return 'export'
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:

View File

@@ -129,8 +129,11 @@ class DekodiNREIExporter(BaseExporter):
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'DT': '30' if invoice.is_cancellation else '10',
'EM': invoice.order.email,
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
'FN': (
invoice.invoice_to_name.rsplit(' ', 1)[0]
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
),
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
'INo': invoice.full_invoice_no,
'IsNet': invoice.reverse_charge,

View File

@@ -278,6 +278,8 @@ class OrderListExporter(MultiSheetListExporter):
]
if self.event.has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
headers += [
_('Product'),
_('Variation'),
@@ -323,7 +325,12 @@ class OrderListExporter(MultiSheetListExporter):
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
row.append(op.subevent)
row.append(op.subevent.name)
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if op.subevent.date_to:
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
row += [
str(op.item),
str(op.variation) if op.variation else '',

View File

@@ -1,6 +1,5 @@
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
@@ -14,32 +13,33 @@ class LoginForm(forms.Form):
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
error_messages = {
'invalid_login': _("Please enter a correct email address and password."),
'invalid_login': _("This combination of credentials is not known to our system."),
'inactive': _("This account is inactive.")
}
def __init__(self, request=None, *args, **kwargs):
def __init__(self, backend, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
self.backend = backend
super().__init__(*args, **kwargs)
for k, f in backend.login_form_fields.items():
self.fields[k] = f
if not settings.PRETIX_LONG_SESSIONS:
del self.fields['keep_logged_in']
else:
self.fields.move_to_end('keep_logged_in')
def clean(self):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if email and password:
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
@@ -181,3 +181,44 @@ class PasswordForgotForm(forms.Form):
def clean_email(self):
return self.cleaned_data['email']
class ReauthForm(forms.Form):
error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."),
'inactive': _("This account is inactive.")
}
def __init__(self, backend, user, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user = user
self.backend = backend
super().__init__(*args, **kwargs)
for k, f in backend.login_form_fields.items():
self.fields[k] = f
if 'email' in self.fields:
self.fields['email'].disabled = True
def clean(self):
self.cleaned_data['email'] = self.user.email
user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if user_cache != self.user:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'
)
else:
self.confirm_login_allowed(user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user: User):
if not user.is_active:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
)

View File

@@ -9,6 +9,7 @@ import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from babel import localedata
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
@@ -21,11 +22,17 @@ from django.utils.translation import (
)
from django_countries import countries
from django_countries.fields import Country, CountryField
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget
from phonenumbers import NumberParseException
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
TimePickerWidget, UploadedFileWidget,
)
from pretix.base.i18n import language
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.settings import (
@@ -41,8 +48,19 @@ from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name']
class NamePartsWidget(forms.MultiWidget):
widget = forms.TextInput
autofill_map = {
'given_name': 'given-name',
'family_name': 'family-name',
'middle_name': 'additional-name',
'title': 'honorific-prefix',
'full_name': 'name',
'calling_name': 'nickname',
}
def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None):
widgets = []
@@ -83,14 +101,21 @@ class NamePartsWidget(forms.MultiWidget):
except (IndexError, TypeError):
widget_value = None
if id_:
final_attrs = dict(
these_attrs = dict(
final_attrs,
id='%s_%s' % (id_, i),
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
final_attrs['data-size'] = self.scheme['fields'][i][2]
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS:
if self.field.required:
these_attrs['required'] = 'required'
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]
else:
these_attrs = final_attrs
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets) -> str:
@@ -150,13 +175,45 @@ class NamePartsFormField(forms.MultiValueField):
def clean(self, value) -> dict:
value = super().clean(value)
if self.one_required and (not value or not any(v for v in value)):
if self.one_required and (not value or not any(v for v in value.values())):
raise forms.ValidationError(self.error_messages['required'], code='required')
if self.one_required:
for k, v in value.items():
if k in REQUIRED_NAME_PARTS and not v:
raise forms.ValidationError(self.error_messages['required'], code='required')
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
return value
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def render(self, name, value, attrs=None, renderer=None):
output = super().render(name, value, attrs, renderer)
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets) -> str:
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
def guess_country(event):
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale in valid_countries:
country = Country(locale.upper())
return country
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -194,7 +251,12 @@ class BaseQuestionsForm(forms.Form):
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
widget=forms.EmailInput(
attrs={
'autocomplete': 'email'
}
)
)
for q in questions:
@@ -301,6 +363,32 @@ class BaseQuestionsForm(forms.Form):
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_PHONENUMBER:
babel_locale = 'en'
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
if localedata.exists(get_language()):
babel_locale = get_language()
elif localedata.exists(get_language()[:2]):
babel_locale = get_language()[:2]
with language(babel_locale):
default_country = guess_country(event)
default_prefix = None
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if str(default_country) in values:
default_prefix = prefix
try:
initial = PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix)
except NumberParseException:
initial = None
field = PhoneNumberField(
label=label, required=required,
help_text=help_text,
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
# the future.
initial=initial,
widget=WrappedPhoneNumberPrefixWidget()
)
field.question = q
if answers:
# Cache the answer object for later use
@@ -324,12 +412,18 @@ class BaseQuestionsForm(forms.Form):
self.fields[key] = value
value.initial = data.get('question_form_data', {}).get(key)
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
d = super().clean()
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
def question_is_visible(parentid, qvals):
if parentid not in question_cache:
return False
parentq = question_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
return False
@@ -366,9 +460,25 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'vat_id', 'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'country': forms.Select(attrs={
'autocomplete': 'country',
}),
'zipcode': forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
'city': forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -384,23 +494,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale in valid_countries:
country = Country(locale.upper())
kwargs['initial']['country'] = country
kwargs['initial']['country'] = guess_country(self.event)
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
@@ -426,7 +520,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c
choices=c,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),
)
self.fields['state'].widget.is_required = True
@@ -456,13 +553,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
if not event.settings.invoice_name_required:
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
def clean(self):
data = self.cleaned_data
if not data.get('is_business'):
@@ -522,9 +624,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
if f != 'name':
if f != 'name_parts':
del self.fields[f]

View File

@@ -56,6 +56,11 @@ class UserSettingsForm(forms.ModelForm):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['email'].required = True
if self.user.auth_backend != 'native':
del self.fields['old_pw']
del self.fields['new_pw']
del self.fields['new_pw_repeat']
self.fields['email'].disabled = True
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')

View File

@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
if value.count('{') != value.count('}'):
raise ValidationError(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
code='invalid',
code='invalid_placeholder_syntax',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid',
code='invalid_placeholders',
params={'value': ", ".join(invalid_placeholders,)})
def clean(self, x):

View File

@@ -46,6 +46,14 @@ class TimePickerWidget(forms.TimeInput):
class UploadedFileWidget(forms.ClearableFileInput):
def __init__(self, *args, **kwargs):
# Browsers can't recognize that the server already has a file uploaded
# Don't mark this input as being required if we already have an answer
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
attrs = kwargs.get('attrs', {})
if kwargs.get('required') and kwargs.get('initial'):
attrs.update({'required': None})
kwargs.update({'attrs': attrs})
self.position = kwargs.pop('position')
self.event = kwargs.pop('event')
self.answer = kwargs.pop('answer')

View File

@@ -457,19 +457,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
id='normal')
]
def _get_story(self, doc):
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not self.invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
def _get_intro(self):
story = []
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
@@ -478,7 +467,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
pgettext('invoice', 'Customer VAT ID') + ': ' +
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
self.stylesheet['Normal']
))
@@ -494,6 +483,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Spacer(1, 10 * mm))
return story
def _get_story(self, doc):
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not self.invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
story += self._get_intro()
taxvalue_map = defaultdict(Decimal)
grossvalue_map = defaultdict(Decimal)

View File

@@ -23,7 +23,7 @@ modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, blacklist of field sub-types)
# (field type, attribute name, banlist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
@@ -38,8 +38,8 @@ original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, blacklist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
kwargs.pop(attr, None)
return name, path, args, kwargs

View File

@@ -11,13 +11,13 @@ from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
blacklist = (
banlist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
)
def write(self, msg, style_func=None, ending=None):
if any(b in msg for b in self.blacklist):
if any(b in msg for b in self.banlist):
return
super().write(msg, style_func, ending)

View File

@@ -14,6 +14,7 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import get_domain
_supported = None
@@ -187,6 +188,11 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = []
gs = GlobalSettingsObject()
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
@@ -196,7 +202,7 @@ class SecurityMiddleware(MiddlewareMixin):
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs

View File

@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -23,12 +23,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -124,7 +124,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(
code=merge_names,

View File

@@ -346,7 +346,7 @@ class Migration(migrations.Migration):
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',

View File

@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -105,7 +105,7 @@ class Migration(migrations.Migration):
'references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -125,7 +125,7 @@ class Migration(migrations.Migration):
' events.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.4 on 2019-10-07 08:03
from django.core.cache import cache
from django.db import migrations
def mail_migrator(app, schema_editor):
Event_SettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for ss in Event_SettingsStore.objects.filter(
key__in=['mail_text_order_approved', 'mail_text_order_placed', 'mail_text_order_placed_require_approval']
):
chgd = ss.value.replace("{date}", "{expire_date}")
if chgd != ss.value:
ss.value = chgd
ss.save()
cache.delete('hierarkey_{}_{}'.format('event', ss.object_id))
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0134_auto_20190909_1042'),
]
operations = [
migrations.RunPython(mail_migrator, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 2.2 on 2019-09-18 17:42
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0135_auto_20191007_0803'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='auto_checked_in',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkinlist',
name='auto_checkin_sales_channels',
field=pretix.base.models.fields.MultiStringField(default=[]),
)
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.4 on 2019-10-15 11:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0136_auto_20190918_1742'),
]
operations = [
migrations.AddField(
model_name='user',
name='auth_backend',
field=models.CharField(default='native', max_length=255),
),
]

View File

@@ -0,0 +1,107 @@
# Generated by Django 2.2.4 on 2019-10-17 11:51
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.fields
import pretix.base.models.giftcards
def fwd(app, schema_editor):
Team = app.get_model('pretixbase', 'Team')
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_gift_cards=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0137_auto_20191015_1141'),
]
operations = [
migrations.CreateModel(
name='GiftCard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('issuance', models.DateTimeField(auto_now_add=True)),
('secret', models.CharField(db_index=True, default=pretix.base.models.giftcards.gen_giftcard_secret,
max_length=190)),
('currency', models.CharField(max_length=10)),
('issued_in', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='issued_gift_cards', to='pretixbase.OrderPosition')),
('issuer',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='issued_gift_cards',
to='pretixbase.Organizer')),
('testmode', django.db.models.BooleanField(default=False)),
],
options={
'unique_together': {('secret', 'issuer')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AddField(
model_name='item',
name='issue_giftcard',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='team',
name='can_manage_gift_cards',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='question',
name='dependency_values',
field=pretix.base.models.fields.MultiStringField(default=[]),
),
migrations.AlterField(
model_name='voucher',
name='item',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.Item'),
),
migrations.AlterField(
model_name='voucher',
name='quota',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.Quota'),
),
migrations.AlterField(
model_name='voucher',
name='variation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.ItemVariation'),
),
migrations.CreateModel(
name='GiftCardTransaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('datetime', models.DateTimeField(auto_now_add=True)),
('value', models.DecimalField(decimal_places=2, max_digits=10)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions',
to='pretixbase.GiftCard')),
('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.Order')),
('payment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.OrderPayment')),
('refund', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.OrderRefund')),
],
options={
'ordering': ('datetime',),
},
),
migrations.CreateModel(
name='GiftCardAcceptance',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('collector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='gift_card_issuer_acceptance', to='pretixbase.Organizer')),
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='gift_card_collector_acceptance', to='pretixbase.Organizer')),
],
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.1 on 2019-10-19 13:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0138_auto_20191017_1151'),
]
operations = [
migrations.AddField(
model_name='event',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='event',
name='geo_lon',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lon',
field=models.FloatField(null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-11-14 11:49
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0139_auto_20191019_1317'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='seat',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers', to='pretixbase.Seat'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-11-22 11:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0140_voucher_seat'),
]
operations = [
migrations.AddField(
model_name='seat',
name='sorting_rank',
field=models.BigIntegerField(default=0),
),
]

View File

@@ -7,6 +7,7 @@ from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
)
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,

View File

@@ -109,6 +109,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
help_text=_('If turned off, you will not get any notifications.')
)
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
auth_backend = models.CharField(max_length=255, default='native')
objects = UserManager()
@@ -334,6 +335,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
)
@scopes_disabled()
def get_organizers_with_permission(self, permission, request=None):
"""
Returns a queryset of organizers the user has a specific permissions to.
:param request: The current request (optional). Required to detect staff sessions properly.
:return: Iterable of Organizers
"""
from .event import Organizer
if request and self.has_active_staff_session(request.session.session_key):
return Organizer.objects.all()
kwargs = {permission: True}
return Organizer.objects.filter(
id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)

View File

@@ -1,11 +1,11 @@
from django.db import models
from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
from django.db.models.functions import Coalesce
from django.db.models import Exists, OuterRef
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
class CheckinList(LoggedModel):
@@ -18,142 +18,69 @@ class CheckinList(LoggedModel):
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid. This only works with pretixdesk '
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
'order have not been paid.'))
auto_checkin_sales_channels = MultiStringField(
default=[],
blank=True,
verbose_name=_('Sales channels to automatically check in'),
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
)
objects = ScopedManager(organizer='event__organizer')
class Meta:
ordering = ('subevent__date_from', 'name')
@property
def positions(self):
from . import OrderPosition, Order
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
subevent=self.subevent
)
if not self.all_products:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
return qs
@property
def checkin_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_checkin_count'.format(self.pk),
lambda: self.positions.annotate(
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))
).filter(
checkedin=True
).count(),
60
)
@property
def percent(self):
pc = self.position_count
return round(self.checkin_count * 100 / pc) if pc else 0
@property
def position_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_position_count'.format(self.pk),
lambda: self.positions.count(),
60
)
def touch(self):
self.event.cache.delete('checkin_list_{}_position_count'.format(self.pk))
self.event.cache.delete('checkin_list_{}_checkin_count'.format(self.pk))
@staticmethod
def annotate_with_numbers(qs, event):
"""
Modifies a queryset of checkin lists by annotating it with the number of order positions and
checkins associated with it.
"""
# Import here to prevent circular import
from . import Order, OrderPosition, Item
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
# First, we prepare a subquery that for every check-in that belongs to a paid-order
# position and to the list in question. Then, we check that it also belongs to the
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
# since we filtered by lists).
cqs_paid = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
cqs_paid_and_pending = Checkin.objects.filter(
position__order__event=event,
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
# Now for the hard part: getting all order positions that contribute to this list. This
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
# lists that contain all the products of the event. This is the simpler one, it basically
# looks like the check-in counter above.
pqs_all_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_all_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Now we need a subquery for the case of checkin lists that are limited to certain
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
# with the products table and we'd get duplicate rows in the output with different annotations
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
# to retrieve all of those items and then check if the item_id is IN this subquery result.
pqs_limited_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
# we want to display a progress bar.
return qs.annotate(
checkin_count=Coalesce(
Case(
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
default=Subquery(cqs_paid, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
),
position_count=Coalesce(
Case(
When(all_products=True, include_pending=False,
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
When(all_products=True, include_pending=True,
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
When(all_products=False, include_pending=False,
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
default=0,
output_field=models.IntegerField()
)
)
# This is only kept for backwards-compatibility reasons. This method used to precompute .position_count
# and .checkin_count through a huge subquery chain, but was dropped for performance reasons.
return qs
def __str__(self):
return self.name
@@ -169,6 +96,7 @@ class Checkin(models.Model):
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
@@ -182,8 +110,11 @@ class Checkin(models.Model):
def save(self, **kwargs):
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
super().save(**kwargs)
def delete(self, **kwargs):
self.position.order.touch()
super().delete(**kwargs)
self.list.touch()

View File

@@ -3,6 +3,7 @@ import uuid
from collections import OrderedDict
from datetime import datetime, time, timedelta
from operator import attrgetter
from urllib.parse import urljoin
import pytz
from django.conf import settings
@@ -11,7 +12,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -22,10 +23,11 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
@@ -145,10 +147,13 @@ class EventMixin:
"@context": "http://schema.org",
"@type": "Event", "location": {
"@type": "Place",
"address": str(self.location)
"address": str(self.location),
},
"name": str(self.name)
"name": str(self.name),
}
img = getattr(self, 'event', self).social_image
if img:
eventdict['image'] = img
if self.settings.show_times:
eventdict["startDate"] = self.date_from.isoformat()
@@ -291,7 +296,7 @@ class Event(EventMixin, LoggedModel):
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBlacklistValidator()
EventSlugBanlistValidator()
],
verbose_name=_("Short form"),
)
@@ -324,6 +329,14 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True,
)
plugins = models.TextField(
null=False, blank=True,
verbose_name=_("Plugins"),
@@ -351,8 +364,31 @@ class Event(EventMixin, LoggedModel):
return str(self.name)
@property
def free_seats(self):
def social_image(self):
from pretix.multidomain.urlreverse import build_absolute_uri
img = None
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
og_file = self.settings.get('og_image', as_type=str, default='')[7:]
if og_file:
img = get_thumbnail(og_file, '1200').thumb.url
elif logo_file:
img = get_thumbnail(logo_file, '5000x120').thumb.url
if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def free_seats(self, ignore_voucher=None):
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
return self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
@@ -367,8 +403,11 @@ class Event(EventMixin, LoggedModel):
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, blocked=False)
).filter(has_order=False, has_cart=False, has_voucher=False, blocked=False)
@property
def presale_has_ended(self):
@@ -624,7 +663,9 @@ class Event(EventMixin, LoggedModel):
pp = p(self)
providers[pp.identifier] = pp
self._cached_payment_providers = OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
self._cached_payment_providers = OrderedDict(sorted(
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
))
return self._cached_payment_providers
def get_html_mail_renderer(self):
@@ -730,7 +771,7 @@ class Event(EventMixin, LoggedModel):
def has_payment_provider(self):
result = False
for provider in self.get_payment_providers().values():
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting'):
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting', 'giftcard'):
result = True
break
return result
@@ -929,6 +970,14 @@ class SubEvent(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")
@@ -949,9 +998,19 @@ class SubEvent(EventMixin, LoggedModel):
def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display())
@property
def free_seats(self):
def free_seats(self, ignore_voucher=None):
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
return self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
@@ -968,8 +1027,11 @@ class SubEvent(EventMixin, LoggedModel):
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, blocked=False)
).filter(has_order=False, has_cart=False, blocked=False, has_voucher=False)
@cached_property
def settings(self):

View File

@@ -0,0 +1,112 @@
from decimal import Decimal
from django.conf import settings
from django.db import models
from django.db.models import Sum
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from pretix.base.banlist import banned
from pretix.base.models import LoggedModel
def gen_giftcard_secret():
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=settings.ENTROPY['giftcard_secret'], allowed_chars=charset)
if not banned(code) and not GiftCard.objects.filter(secret=code).exists():
return code
class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey(
'Organizer',
related_name='gift_card_collector_acceptance',
on_delete=models.CASCADE
)
collector = models.ForeignKey(
'Organizer',
related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE
)
class GiftCard(LoggedModel):
issuer = models.ForeignKey(
'Organizer',
related_name='issued_gift_cards',
on_delete=models.PROTECT,
)
issued_in = models.ForeignKey(
'OrderPosition',
related_name='issued_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True
)
issuance = models.DateTimeField(
auto_now_add=True,
)
secret = models.CharField(
max_length=190,
default=gen_giftcard_secret,
db_index=True,
verbose_name=_('Gift card code'),
)
testmode = models.BooleanField(
verbose_name=_('Test mode card'),
default=False
)
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES)
def __str__(self):
return self.secret
@property
def value(self):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
class Meta:
unique_together = (('secret', 'issuer'),)
class GiftCardTransaction(models.Model):
card = models.ForeignKey(
'GiftCard',
related_name='transactions',
on_delete=models.PROTECT
)
datetime = models.DateTimeField(
auto_now_add=True
)
value = models.DecimalField(
decimal_places=2,
max_digits=10
)
order = models.ForeignKey(
'Order',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
payment = models.ForeignKey(
'OrderPayment',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
refund = models.ForeignKey(
'OrderRefund',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
class Meta:
ordering = ("datetime",)

View File

@@ -175,8 +175,6 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
q &= Q(pk=voucher.item_id)
elif voucher.quota_id:
q &= Q(quotas__in=[voucher.quota_id])
else:
return qs.none()
if not voucher or not voucher.show_hidden_items:
q &= Q(hide_without_voucher=False)
@@ -242,6 +240,8 @@ class Item(LoggedModel):
:type require_approval: bool
:param sales_channels: Sales channels this item is available on.
:type sales_channels: bool
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
:type issue_giftcard: bool
"""
objects = ItemQuerySetManager()
@@ -413,6 +413,12 @@ class Item(LoggedModel):
verbose_name=_('Sales channels'),
default=['web']
)
issue_giftcard = models.BooleanField(
verbose_name=_('This product is a gift card'),
help_text=_('When a customer buys this product, they will get a gift card with a value corresponding to the '
'product price.'),
default=False
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
@@ -969,6 +975,7 @@ class Question(LoggedModel):
TYPE_TIME = "H"
TYPE_DATETIME = "W"
TYPE_COUNTRYCODE = "CC"
TYPE_PHONENUMBER = "TEL"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
@@ -981,8 +988,10 @@ class Question(LoggedModel):
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
(TYPE_PHONENUMBER, _("Phone number")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER]
event = models.ForeignKey(
Event,
@@ -1025,8 +1034,6 @@ class Question(LoggedModel):
)
ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
help_text=_('This will only work if you handle your check-in with pretixdroid 1.8 or newer or '
'pretixdesk 0.2 or newer.'),
default=False
)
hidden = models.BooleanField(

View File

@@ -30,13 +30,18 @@ from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import NumberParseException
from pretix.base.banlist import banned
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
@@ -199,7 +204,7 @@ class Order(LockModel, LoggedModel):
return self.full_code
def gracefully_delete(self, user=None, auth=None):
from . import Voucher
from . import Voucher, GiftCard, GiftCardTransaction
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
@@ -210,11 +215,17 @@ class Order(LockModel, LoggedModel):
}
)
order_gracefully_delete.send(self.event, order=self)
if self.status != Order.STATUS_CANCELED:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None)
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
GiftCardTransaction.objects.filter(order=self).update(order=None)
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
@@ -456,11 +467,15 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item')
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
for gc in op.issued_gift_cards.all():
if gc.value != op.price:
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:
@@ -535,16 +550,30 @@ class Order(LockModel, LoggedModel):
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
# might include OCR'd handwritten text
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
iteration = 0
length = settings.ENTROPY['order_code']
while True:
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
code = get_random_string(length=length, allowed_chars=charset)
iteration += 1
if banned(code):
continue
if self.testmode:
# Subtle way to recognize test orders while debugging: They all contain a 0 at the second place,
# even though zeros are not used outside test mode.
code = code[0] + "0" + code[2:]
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
self.code = code
return
if iteration > 20:
# Safeguard: If we don't find an unused and non-blacklisted code within 20 iterations, we increase
# the length.
length += 1
iteration = 0
@property
def can_modify_answers(self) -> bool:
"""
@@ -697,7 +726,8 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True):
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
attach_ical=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -714,6 +744,7 @@ class Order(LockModel, LoggedModel):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
:param position: An order position this refers to. If given, no invoices will be attached, the tickets will
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
@@ -737,7 +768,7 @@ class Order(LockModel, LoggedModel):
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email
position=position, auto_email=auto_email, attach_ical=attach_ical
)
except SendMailException:
raise
@@ -753,35 +784,19 @@ class Order(LockModel, LoggedModel):
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
}
)
def resend_link(self, user=None, auth=None):
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.locale):
try:
invoice_name = self.invoice_address.name
invoice_company = self.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.event.settings.mail_text_resend_link
email_context = {
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
'order': self.code,
'secret': self.secret,
'hash': self.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=self.event, order=self)
email_subject = _('Your order: %(code)s') % {'code': self.code}
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
attach_tickets=True
attach_tickets=True,
)
@property
@@ -909,6 +924,11 @@ class QuestionAnswer(models.Model):
return self.answer
elif self.question.type == Question.TYPE_COUNTRYCODE and self.answer:
return Country(self.answer).name or self.answer
elif self.question.type == Question.TYPE_PHONENUMBER and self.answer:
try:
return PhoneNumber.from_string(self.answer).as_international
except NumberParseException:
return self.answer
else:
return self.answer
@@ -1044,6 +1064,8 @@ class AbstractPosition(models.Model):
}
def question_is_visible(parentid, qvals):
if parentid not in question_cache:
return False
parentq = question_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
return False
@@ -1198,6 +1220,7 @@ class OrderPayment(models.Model):
"""
return self.order.event.get_payment_providers(cached=True).get(self.provider)
@transaction.atomic()
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
@@ -1266,7 +1289,7 @@ class OrderPayment(models.Model):
'provider': self.provider,
}, user=user, auth=auth)
if self.order.status == Order.STATUS_PAID:
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED):
return
payment_sum = self.order.payments.filter(
@@ -1313,66 +1336,36 @@ class OrderPayment(models.Model):
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[], position=position,
attach_tickets=True
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@@ -1914,25 +1907,26 @@ class OrderPosition(AbstractPosition):
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
if not self.attendee_email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
with language(self.order.locale):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.log_action(
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
@@ -1945,6 +1939,18 @@ class OrderPosition(AbstractPosition):
}
)
def resend_link(self, user=None, auth=None):
with language(self.order.locale):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
attach_tickets=True
)
class CartPosition(AbstractPosition):
"""

View File

@@ -2,12 +2,13 @@ import string
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBlacklistValidator
from pretix.base.validators import OrganizerSlugBanlistValidator
from ..settings import settings_hierarkey
from .auth import User
@@ -39,7 +40,7 @@ class Organizer(LoggedModel):
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes.")
),
OrganizerSlugBlacklistValidator()
OrganizerSlugBanlistValidator()
],
verbose_name=_("Short form"),
unique=True
@@ -82,6 +83,24 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
@property
def has_gift_cards(self):
return self.cache.get_or_set(
key='has_gift_cards',
timeout=15,
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
)
@property
def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.annotate(
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
).filter(
Q(issuer=self) | Q(accepted=True)
)
def allow_delete(self):
from . import Order, Invoice
return (
@@ -156,6 +175,10 @@ class Team(LoggedModel):
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
'reports, so be careful who you add to this team!')
)
can_manage_gift_cards = models.BooleanField(
default=False,
verbose_name=_("Can manage gift cards")
)
can_change_event_settings = models.BooleanField(
default=False,

View File

@@ -5,6 +5,7 @@ import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Q
from django.utils.deconstruct import deconstructible
from django.utils.timezone import now
from django.utils.translation import gettext, ugettext_lazy as _
@@ -39,7 +40,7 @@ class SeatingPlan(LoggedModel):
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
Category = namedtuple('Categrory', 'name')
RawSeat = namedtuple('Seat', 'name guid number row category zone')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank')
def __str__(self):
return self.name
@@ -59,16 +60,36 @@ class SeatingPlan(LoggedModel):
]
def iter_all_seats(self):
for z in self.layout_data['zones']:
for r in z['rows']:
for s in r['seats']:
# This returns all seats in a plan and assignes each of them a rank. The rank is used for sorting lists of
# seats later. The rank does not say anything about the *quality* of a seat, and is only meant as a heuristic
# to make it easier for humas to process lists of seats. The current algorithm assumes that there are less
# than 10'000 zones, less than 10'000 rows in every zone and less than 10'000 seats in every row.
# Respectively, no row/seat numbers may be numeric with a value of 10'000 or more. The resulting ranks
# *will* have gaps. We chose this way over just sorting the seats and continuously enumerating them as an
# optimization, because this way we do not need to update the rank of very seat if we change a plan a little.
for zi, z in enumerate(self.layout_data['zones']):
for ri, r in enumerate(z['rows']):
try:
row_rank = int(r['row_number'])
except ValueError:
row_rank = ri
for si, s in enumerate(r['seats']):
try:
seat_rank = int(s['seat_number'])
except ValueError:
seat_rank = si
rank = (
10000 * 10000 * zi + 10000 * row_rank + seat_rank
)
yield self.RawSeat(
number=s['seat_number'],
guid=s['seat_guid'],
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
row=r['row_number'],
zone=z['name'],
category=s['category']
category=s['category'],
sorting_rank=rank
)
@@ -97,6 +118,10 @@ class Seat(models.Model):
seat_guid = models.CharField(max_length=190, db_index=True)
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
blocked = models.BooleanField(default=False)
sorting_rank = models.BigIntegerField(default=0)
class Meta:
ordering = ['sorting_rank', 'seat_guid']
def __str__(self):
parts = []
@@ -110,15 +135,21 @@ class Seat(models.Model):
return self.name
return ', '.join(parts)
def is_available(self, ignore_cart=None, ignore_orderpos=None):
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None):
from .orders import Order
if self.blocked:
return False
opqs = self.orderposition_set.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
cpqs = self.cartposition_set.filter(expires__gte=now())
if ignore_cart:
vqs = self.vouchers.filter(
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
Q(redeemed__lt=F('max_usages'))
)
if ignore_cart and ignore_cart is not True:
cpqs = cpqs.exclude(pk=ignore_cart.pk)
if ignore_orderpos:
opqs = opqs.exclude(pk=ignore_orderpos.pk)
return not opqs.exists() and not cpqs.exists()
if ignore_voucher_id:
vqs = vqs.exclude(pk=ignore_voucher_id)
return not opqs.exists() and (ignore_cart is True or not cpqs.exists()) and not vqs.exists()

View File

@@ -198,8 +198,14 @@ class TaxRule(LoggedModel):
rate=self.rate, name=self.name
)
@property
def _custom_rules(self):
if not self.custom_rules:
return []
return json.loads(self.custom_rules)
def get_matching_rule(self, invoice_address):
rules = json.loads(self.custom_rules)
rules = self._custom_rules
if invoice_address:
for r in rules:
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
@@ -216,7 +222,7 @@ class TaxRule(LoggedModel):
return {'action': 'vat'}
def is_reverse_charge(self, invoice_address):
if self.custom_rules:
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule['action'] == 'reverse'
@@ -238,7 +244,7 @@ class TaxRule(LoggedModel):
return False
def tax_applicable(self, invoice_address):
if self.custom_rules:
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule.get('action', 'vat') == 'vat'

View File

@@ -10,7 +10,8 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import SeatCategoryMapping
from pretix.base.banlist import banned
from pretix.base.models import Seat, SeatCategoryMapping
from ..decimal import round_decimal
from .base import LoggedModel
@@ -21,9 +22,12 @@ from .orders import Order
def _generate_random_code(prefix=None):
charset = list('ABCDEFGHKLMNPQRSTUVWXYZ23456789')
rnd = None
while not rnd or banned(rnd):
rnd = get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
if prefix:
return prefix + get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
return get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
return prefix + rnd
return rnd
@scopes_disabled()
@@ -167,6 +171,12 @@ class Voucher(LoggedModel):
"If enabled, the voucher is valid for any product affected by this quota."
)
)
seat = models.ForeignKey(
Seat, related_name='vouchers',
null=True, blank=True,
on_delete=models.PROTECT,
verbose_name=_("Specific seat"),
)
tag = models.CharField(
max_length=255,
verbose_name=_("Tag"),
@@ -207,11 +217,12 @@ class Voucher(LoggedModel):
self.event,
self.quota,
self.item,
self.variation
self.variation,
seats_given=bool(self.seat)
)
@staticmethod
def clean_item_properties(data, event, quota, item, variation):
def clean_item_properties(data, event, quota, item, variation, block_quota=False, seats_given=False):
if quota:
if quota.event != event:
raise ValidationError(_('You cannot select a quota that belongs to a different event.'))
@@ -230,8 +241,12 @@ class Voucher(LoggedModel):
'Otherwise it might be unclear which quotas to block.'))
if item.category and item.category.is_addon:
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
elif block_quota:
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
'tickets.'))
elif variation:
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
'variations.'))
@staticmethod
def clean_max_usages(data, redeemed):
@@ -320,7 +335,8 @@ class Voucher(LoggedModel):
elif item and not item.has_variations:
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
'tickets.'))
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
@@ -331,6 +347,42 @@ class Voucher(LoggedModel):
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
@staticmethod
def clean_seat_id(data, item, quota, event, pk):
try:
if event.has_subevents:
if not data.get('subevent'):
raise ValidationError(_('You need to choose a date if you select a seat.'))
seat = event.seats.select_related('product').get(
seat_guid=data.get('seat'), subevent=data.get('subevent')
)
else:
seat = event.seats.select_related('product').get(
seat_guid=data.get('seat')
)
except Seat.DoesNotExist:
raise ValidationError(_('The specified seat ID "{id}" does not exist for this event.').format(
id=data.get('seat')))
if not seat.is_available(ignore_voucher_id=pk, ignore_cart=True):
raise ValidationError(_('The seat "{id}" is currently unavailable (blocked, already sold or a '
'different voucher).').format(
id=seat.seat_guid))
if quota:
raise ValidationError(_('You need to choose a specific product if you select a seat.'))
if data.get('max_usages', 1) > 1:
raise ValidationError(_('Seat-specific vouchers can only be used once.'))
if item and seat.product != item:
raise ValidationError(_('You need to choose the product "{prod}" for this seat.').format(prod=seat.product))
if not seat.is_available(ignore_voucher_id=pk):
raise ValidationError(_('The seat "{id}" is already sold or currently blocked.').format(id=seat.seat_guid))
return seat
def save(self, *args, **kwargs):
self.code = self.code.upper()
super().save(*args, **kwargs)
@@ -363,7 +415,9 @@ class Voucher(LoggedModel):
return item.quotas.filter(pk=self.quota_id).exists()
if self.item_id and not self.variation_id:
return self.item_id == item.pk
return (self.item_id == item.pk) and (variation and self.variation_id == variation.pk)
if self.item_id:
return (self.item_id == item.pk) and (variation and self.variation_id == variation.pk)
return True
def is_active(self):
"""

View File

@@ -6,10 +6,10 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
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.multidomain.urlreverse import build_absolute_uri
from .base import LoggedModel
from .event import Event, SubEvent
@@ -130,13 +130,7 @@ class WaitingListEntry(LoggedModel):
self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)),
self.event.settings.mail_text_waiting_list,
{
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.redeem') + '?voucher=' + self.voucher.code,
'code': self.voucher.code,
'product': str(self.item) + (' - ' + str(self.variation) if self.variation else ''),
'hours': self.event.settings.waiting_list_hours,
},
get_email_context(event=self.event, waiting_list_entry=self),
self.event,
locale=self.locale
)

View File

@@ -3,7 +3,7 @@ from collections import OrderedDict, namedtuple
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
@@ -175,12 +175,23 @@ class ParametrizedOrderNotificationType(NotificationType):
url=order_url
)
n.add_attribute(_('Event'), order.event.name)
if order.event.has_subevents:
ses = []
for se in self.event.subevents.filter(id__in=order.positions.values_list('subevent', flat=True)):
ses.append('{} ({})'.format(se.name, se.get_date_range_display()))
n.add_attribute(pgettext_lazy('subevent', 'Dates'), '\n'.join(ses))
else:
n.add_attribute(_('Event date'), order.event.get_date_range_display())
n.add_attribute(_('Order code'), order.code)
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
n.add_attribute(_('Pending amount'), money_filter(order.pending_sum, logentry.event.currency))
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
n.add_attribute(_('Order status'), order.get_status_display())
n.add_attribute(_('Order positions'), str(order.positions.count()))
items = []
for it in self.event.items.filter(id__in=order.positions.values_list('item', flat=True)):
items.append(str(it.name))
n.add_attribute(_('Purchased products'), '\n'.join(items))
n.add_action(_('View order details'), order_url)
return n

View File

@@ -7,7 +7,9 @@ from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.dispatch import receiver
from django.forms import Form
from django.http import HttpRequest
@@ -18,10 +20,11 @@ from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
Quota,
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -29,7 +32,8 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput
from pretix.presale.views import get_cart_total
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import get_cart, get_cart_total
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
@@ -59,12 +63,11 @@ class BasePaymentProvider:
def __str__(self):
return self.identifier
@property
def is_implicit(self) -> bool:
def is_implicit(self, request: HttpRequest) -> bool:
"""
Returns whether or whether not this payment provider is an "implicit" payment provider that will
*always* and unconditionally be used if is_allowed() returns True and does not require any input.
This is intended to be used by the FreePaymentProvider, which skips the payment choice page.
This is intended to be used by the FreeOrderProvider, which skips the payment choice page.
By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
"""
return False
@@ -80,6 +83,14 @@ class BasePaymentProvider:
"""
return False
@property
def priority(self) -> int:
"""
Returns a priority that is used for sorting payment providers. Higher priority means higher up in the list.
Default to 100. Providers with same priority are sorted alphabetically.
"""
return 100
@property
def is_enabled(self) -> bool:
"""
@@ -275,8 +286,21 @@ class BasePaymentProvider:
required=False,
disabled=not self.event.settings.invoice_address_required
)),
('_restrict_to_sales_channels',
forms.MultipleChoiceField(
label=_('Restrict to specific sales channels'),
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
if c.payment_restrictions_supported
),
initial=['web'],
widget=forms.CheckboxSelectMultiple,
help_text=_(
'Only allow the usage of this payment provider in the following sales channels'),
))
])
d['_restricted_countries']._as_type = list
d['_restrict_to_sales_channels']._as_type = list
return d
def settings_form_clean(self, cleaned_data):
@@ -388,7 +412,7 @@ class BasePaymentProvider:
The default implementation checks for the _availability_date setting to be either unset or in the future
and for the _total_max and _total_min requirements to be met. It also checks the ``_restrict_countries``
setting.
and ``_restrict_to_sales_channels`` setting.
:param total: The total value without the payment method fee, after taxes.
@@ -429,6 +453,10 @@ class BasePaymentProvider:
if str(ia.country) not in restricted_countries:
return False
if hasattr(request, 'sales_channel') and request.sales_channel.identifier not in \
self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False
return timing and pricing
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
@@ -531,7 +559,7 @@ class BasePaymentProvider:
containing the URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page.
If the payment is completed, you should call ``payment.confirm()``. Please note that ``this`` might
If the payment is completed, you should call ``payment.confirm()``. Please note that this might
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
some of the items are sold out. You should use the exception message to display a meaningful error
to the user.
@@ -584,6 +612,9 @@ class BasePaymentProvider:
if str(ia.country) not in restricted_countries:
return False
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False
return self._is_still_available(order=order)
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
@@ -620,6 +651,19 @@ class BasePaymentProvider:
"""
return ''
def refund_control_render(self, request: HttpRequest, refund: OrderRefund) -> str:
"""
Will be called if the *event administrator* views the details of a refund.
It should return HTML code containing information regarding the current refund
status and, if applicable, next steps.
The default implementation returns an empty string.
:param order: The order object
"""
return ''
def payment_refund_supported(self, payment: OrderPayment) -> bool:
"""
Will be called to check if the provider supports automatic refunding for this
@@ -634,6 +678,17 @@ class BasePaymentProvider:
"""
return False
def cancel_payment(self, payment: OrderPayment):
"""
Will be called to cancel a payment. The default implementation just sets the payment state to canceled,
but in some cases you might want to notify an external provider.
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
On failure, you should raise a PaymentException.
"""
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
def execute_refund(self, refund: OrderRefund):
"""
Will be called to execute an refund. Note that refunds have an amount property and can be partial.
@@ -699,8 +754,9 @@ class FreeOrderProvider(BasePaymentProvider):
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
from .services.cart import get_fees
cart = get_cart(request)
total = get_cart_total(request)
total += sum([f.value for f in get_fees(self.event, request, total, None, None)])
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
return total == 0
def order_change_allowed(self, order: Order) -> bool:
@@ -761,8 +817,7 @@ class ManualPayment(BasePaymentProvider):
return _('In test mode, you can just manually mark this order as paid in the backend after it has been '
'created.')
@property
def is_implicit(self):
def is_implicit(self, request: HttpRequest):
return 'pretix.plugins.manualpayment' not in self.event.plugins
def is_allowed(self, request: HttpRequest, total: Decimal=None):
@@ -867,7 +922,10 @@ class OffsettingProvider(BasePaymentProvider):
provider='offsetting',
info=json.dumps({'orders': [refund.order.code]})
)
p.confirm()
try:
p.confirm(ignore_date=True)
except Quota.QuotaExceededException:
pass
@property
def settings_form_fields(self) -> dict:
@@ -888,6 +946,222 @@ class OffsettingProvider(BasePaymentProvider):
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))
class GiftCardPayment(BasePaymentProvider):
identifier = "giftcard"
verbose_name = _("Gift card")
priority = 10
@property
def settings_form_fields(self):
f = super().settings_form_fields
del f['_fee_abs']
del f['_fee_percent']
del f['_fee_reverse_calc']
del f['_total_min']
del f['_total_max']
del f['_invoice_text']
return f
@property
def test_mode_message(self) -> str:
return _("In test mode, only test cards will work.")
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return super().is_allowed(request, total) and self.event.organizer.has_gift_cards
def order_change_allowed(self, order: Order) -> bool:
return super().order_change_allowed(order) and self.event.organizer.has_gift_cards
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
return get_template('pretixcontrol/giftcards/checkout.html').render({})
def checkout_confirm_render(self, request) -> str:
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({})
def refund_control_render(self, request, refund) -> str:
from .models import GiftCard
if 'gift_card' in refund.info_data:
gc = GiftCard.objects.get(pk=refund.info_data.get('gift_card'))
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
}
return template.render(ctx)
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
}
return template.render(ctx)
def api_payment_details(self, payment: OrderPayment):
from .models import GiftCard
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug
}
}
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
return True
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
for p in get_cart(request):
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
cs = cart_session(request)
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.testmode and not self.event.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
if 'gift_cards' not in cs:
cs['gift_cards'] = []
elif gc.pk in cs['gift_cards']:
messages.error(request, _("This gift card is already used for your payment."))
return
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
remainder = cart['total'] - gc.value
if remainder >= Decimal('0.00'):
del cs['payment']
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
money_filter(remainder, self.event.currency)
))
else:
messages.success(request, _("Your gift card has been applied."))
kwargs = {'step': 'payment'}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str, None]:
for p in payment.order.positions.all():
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.testmode and not payment.order.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
payment.info_data = {
'gift_card': gc.pk,
'retry': True
}
payment.amount = min(payment.amount, gc.value)
payment.save()
return True
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
# This method will only be called when retrying payments, e.g. after a payment_prepare call. It is not called
# during the order creation phase because this payment provider is a special case.
for p in payment.order.positions.all(): # noqa - just a safeguard
if p.item.issue_giftcard:
raise PaymentException(_("You cannot pay with gift cards when buying a gift card."))
gcpk = payment.info_data.get('gift_card')
if not gcpk or not payment.info_data.get('retry'):
raise PaymentException("Invalid state, should never occur.")
with transaction.atomic():
gc = GiftCard.objects.select_for_update().get(pk=gcpk)
if gc.currency != self.event.currency: # noqa - just a safeguard
raise PaymentException(_("This gift card does not support this currency."))
if not gc.accepted_by(self.event.organizer): # noqa - just a safeguard
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again"))
trans = gc.transactions.create(
value=-1 * payment.amount,
order=payment.order,
payment=payment
)
payment.info_data = {
'gift_card': gc.pk,
'transaction_id': trans.pk,
}
payment.confirm()
def payment_is_valid_session(self, request: HttpRequest) -> bool:
return True
@transaction.atomic()
def execute_refund(self, refund: OrderRefund):
from .models import GiftCard
gc = GiftCard.objects.get(pk=refund.info_data.get('gift_card') or refund.payment.info_data.get('gift_card'))
trans = gc.transactions.create(
value=refund.amount,
order=refund.order,
refund=refund
)
refund.info_data = {
'gift_card': gc.pk,
'transaction_id': trans.pk,
}
refund.done()
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment]
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]

View File

@@ -14,6 +14,7 @@ from django.conf import settings
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from PyPDF2 import PdfFileReader
@@ -52,35 +53,40 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": "A1B2C",
"evaluate": lambda orderposition, order, event: orderposition.order.code
}),
("positionid", {
"label": _("Order position number"),
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.name)
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.name))
}),
("variation", {
"label": _("Variation name"),
"editor_sample": _("Sample variation"),
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
"evaluate": lambda op, order, event: escape(str(op.variation) if op.variation else '')
}),
("item_description", {
"label": _("Product description"),
"editor_sample": _("Sample product description"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.description))
}),
("itemvar", {
"label": _("Product name and variation"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: (
"evaluate": lambda orderposition, order, event: escape((
'{} - {}'.format(orderposition.item.name, orderposition.variation)
if orderposition.variation else str(orderposition.item.name)
)
))
}),
("item_category", {
"label": _("Product category"),
"editor_sample": _("Ticket category"),
"evaluate": lambda orderposition, order, event: (
"evaluate": lambda orderposition, order, event: escape((
str(orderposition.item.category.name) if orderposition.item.category else ""
)
))
}),
("price", {
"label": _("Price"),
@@ -99,12 +105,12 @@ DEFAULT_VARIABLES = OrderedDict((
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
"evaluate": lambda op, order, ev: escape(op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''))
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(ev.name)
"evaluate": lambda op, order, ev: escape(str(ev.name))
}),
("event_date", {
"label": _("Event date"),
@@ -185,12 +191,12 @@ DEFAULT_VARIABLES = OrderedDict((
("invoice_name", {
"label": _("Invoice address name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
"evaluate": lambda op, order, ev: escape(order.invoice_address.name if getattr(order, 'invoice_address', None) else '')
}),
("invoice_company", {
"label": _("Invoice address company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
"evaluate": lambda op, order, ev: escape(order.invoice_address.company if getattr(order, 'invoice_address', None) else '')
}),
("addons", {
"label": _("List of Add-Ons"),
@@ -207,7 +213,7 @@ DEFAULT_VARIABLES = OrderedDict((
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
"evaluate": lambda op, order, ev: escape(str(order.event.organizer.name))
}),
("organizer_info_text", {
"label": _("Organizer info text"),
@@ -266,16 +272,28 @@ DEFAULT_VARIABLES = OrderedDict((
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
def variables_from_questions(sender, *args, **kwargs):
def get_answer(op, order, event, question_id):
try:
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
a = [a for a in op.answers.all() if a.question_id == question_id][0]
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.answers.filter(question_id=question_id).first()
if not a:
return ""
return str(a).replace("\n", "<br/>\n")
except IndexError:
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:
return ""
else:
return escape(str(a)).replace("\n", "<br/>\n")
d = {}
for q in sender.questions.all():
@@ -288,11 +306,11 @@ def variables_from_questions(sender, *args, **kwargs):
def _get_attendee_name_part(key, op, order, ev):
return op.attendee_name_parts.get(key, '')
return escape(op.attendee_name_parts.get(key, ''))
def _get_ia_name_part(key, op, order, ev):
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
return escape(order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else '')
def get_variables(event):

View File

@@ -103,11 +103,18 @@ class RelativeDateWrapper:
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
data = RelativeDate(
days_before=int(parts[1] or 0),
base_date_name=parts[3],
time=time
)
try:
data = RelativeDate(
days_before=int(parts[1] or 0),
base_date_name=parts[3],
time=time
)
except ValueError:
data = RelativeDate(
days_before=0,
base_date_name=parts[3],
time=time
)
else:
data = parser.parse(input)
return RelativeDateWrapper(data)

View File

@@ -12,6 +12,7 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext as _
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat,
@@ -80,6 +81,7 @@ error_messages = {
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
'voucher_item_not_available': _(
'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
@@ -97,6 +99,7 @@ error_messages = {
'seat_forbidden': _('You can not select a seat for this position.'),
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': _('You can not select the same seat multiple times.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
}
@@ -216,13 +219,14 @@ class CartManager:
})
def _check_max_cart_size(self):
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
def _check_item_constraints(self, op):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
@@ -251,6 +255,9 @@ class CartManager:
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
if op.voucher and op.voucher.seat and op.voucher.seat != op.seat:
raise CartError(error_messages['voucher_invalid_seat'])
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
raise CartError(error_messages['voucher_invalid_subevent'])
@@ -823,7 +830,7 @@ class CartManager:
available_count = 0
if isinstance(op, self.AddOperation):
if op.seat and not op.seat.is_available():
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None):
available_count = 0
err = err or error_messages['seat_unavailable']
@@ -871,7 +878,8 @@ class CartManager:
new_cart_positions.append(cp)
elif isinstance(op, self.ExtendOperation):
if op.seat and not op.seat.is_available(ignore_cart=op.position):
if op.seat and not op.seat.is_available(ignore_cart=op.position,
ignore_voucher_id=op.position.voucher_id):
err = err or error_messages['seat_unavailable']
op.position.addons.all().delete()
op.position.delete()
@@ -957,15 +965,42 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
return totaldiff
def get_fees(event, request, total, invoice_address, provider):
fees = []
def get_fees(event, request, total, invoice_address, provider, positions):
from pretix.presale.views.cart import cart_session
fees = []
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
total=total):
total=total, positions=positions):
if resp:
fees += resp
total = total + sum(f.value for f in fees)
cs = cart_session(request)
if cs.get('gift_cards'):
gcs = cs['gift_cards']
gc_qs = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency)
summed = 0
for gc in gc_qs:
if gc.testmode != event.testmode:
gcs.remove(gc.pk)
continue
fval = Decimal(gc.value) # TODO: don't require an extra query
fval = min(fval, total - summed)
if fval > 0:
total -= fval
summed += fval
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_GIFTCARD,
internal_type='giftcard',
description=gc.secret,
value=-1 * fval,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=TaxRule.zero()
))
cs['gift_cards'] = gcs
if provider and total != 0:
provider = event.get_payment_providers().get(provider)
if provider:

View File

@@ -1,11 +1,13 @@
from django.db import transaction
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.models import (
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.signals import order_placed
class CheckInError(Exception):
@@ -155,3 +157,18 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'datetime': dt,
'list': clist.pk
}, user=user, auth=auth)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
Checkin.objects.create(position=op, list=cl, auto_checked_in=True)

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