Compare commits

...

147 Commits

Author SHA1 Message Date
Raphael Michel
31d1fc31cd Bump version to 3.8.0 2020-04-17 17:19:02 +02:00
Raphael Michel
597211d83a Merge pull request #1646 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-17 17:11:24 +02:00
Raphael Michel
17679d4304 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3609 of 3609 strings)

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

powered by weblate
2020-04-17 17:11:08 +02:00
Raphael Michel
0fb70c78a9 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3609 of 3609 strings)

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

powered by weblate
2020-04-17 17:11:07 +02:00
Raphael Michel
e254e90e49 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-04-17 16:26:52 +02:00
Raphael Michel
9c6e5f025d Merge pull request #1642 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-17 16:26:21 +02:00
Raphael Michel
3c86532218 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-17 16:25:29 +02:00
Raphael Michel
3834ae566f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-17 16:25:29 +02:00
Raphael Michel
6766b2b19e Fix #1645 -- Allow one-letter event slugs 2020-04-17 16:25:04 +02:00
Raphael Michel
b6d2f67c7c Cache sorting of countries 2020-04-17 13:21:13 +02:00
Raphael Michel
e70f593a94 Minor SQL performance improvements 2020-04-17 12:14:37 +02:00
Raphael Michel
ed5726fc0c Merge branch 'master' of github.com:pretix/pretix 2020-04-17 12:04:41 +02:00
Martin Gross
5400d26c60 Log the reason for failed PayPal refunds 2020-04-17 12:02:43 +02:00
Raphael Michel
0bb6104532 Flip order of invoices and tickets in email attachments 2020-04-16 13:14:06 +02:00
Raphael Michel
16aa403735 Fix incorrect checkin list tests 2020-04-15 13:06:24 +02:00
Martin Gross
1c279a92a7 Merge pull request #1643 from pretix/event_cancellation_giftcard_refund
Allow to issue gift card refunds when cancelling whole events
2020-04-15 13:00:41 +02:00
Martin Gross
35985dcb11 Remove legal warning, make refund_as_giftcard-option clearer. 2020-04-15 12:58:43 +02:00
Raphael Michel
b0dcbe31fa Fix incorrect quota error when changing subevent and item of a position 2020-04-15 12:54:57 +02:00
Martin Gross
b3c3ee3b22 Allow to issue gift card refunds when cancelling whole events 2020-04-15 10:08:12 +02:00
Raphael Michel
aab340fd87 Fix missing translation context 2020-04-15 09:43:58 +02:00
Raphael Michel
1871324ef4 Restrict length of item name in quick setup
PRETIXEU-21V
2020-04-15 09:20:55 +02:00
Raphael Michel
d799d560b7 Fix typo in settings definition 2020-04-14 09:47:47 +02:00
Raphael Michel
01e2851a76 Invoice model: Do not crash on invalid states 2020-04-14 09:47:34 +02:00
Raphael Michel
ef2a4244ed Merge branch 'master' of github.com:pretix/pretix 2020-04-09 09:29:58 +02:00
Raphael Michel
55539dc8e5 Fix confusing background color in subevents 2020-04-09 09:29:40 +02:00
Martin Gross
ef303bfcc4 Add cancel_allow_user_paid_adjust_fees_explanation (#1639)
* Add cancel_allow_user_paid_adjust_fees_explanation

* Cleanup
2020-04-08 17:49:36 +02:00
Martin Gross
fff9ac04a9 Fix test (Caused by 31fdf8721b) 2020-04-08 17:49:16 +02:00
Martin Gross
76d27fbfaa Cleanup 2020-04-08 17:08:58 +02:00
Martin Gross
2b1123b487 Add cancel_allow_user_paid_adjust_fees_explanation 2020-04-08 16:43:29 +02:00
pretix translation bot
3607d8706d Translations update from Weblate (#1638)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

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

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate

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

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

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

Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate

Co-authored-by: Martin Gross <martin@pc-coholic.de>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-04-08 15:48:55 +02:00
Raphael Michel
31fdf8721b API: Fix selecting checkin question answers by option identifier 2020-04-07 15:16:58 +02:00
Raphael Michel
128a1f349a Revert accidentally commited change 2020-04-03 14:47:44 +02:00
Raphael Michel
7d432f0639 Improve display of attendee addresses 2020-04-03 14:35:38 +02:00
Raphael Michel
1ffc799c4d Split banner text into top and bottom 2020-04-03 13:02:23 +02:00
Raphael Michel
25dd8f2e2f Introduce banner text 2020-04-02 18:35:28 +02:00
Raphael Michel
b121596e4b Merge pull request #1635 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-02 18:28:50 +02:00
Raphael Michel
cf835df62e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-02 18:28:27 +02:00
Raphael Michel
7a3b7d4f02 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-02 18:28:26 +02:00
Maarten van den Berg
b151d8f455 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3590 of 3590 strings)

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

powered by weblate
2020-04-02 18:17:28 +02:00
Raphael Michel
06de74d877 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-04-02 18:17:18 +02:00
Raphael Michel
2ae9e3e0d9 Directly load editor on supported browsers 2020-04-02 18:16:33 +02:00
Raphael Michel
0c0fe58bbf Improve UX of ticket download settings 2020-04-02 18:16:22 +02:00
Raphael Michel
7b1e1a48ef Enable PDF and passbook outputs for new events by default 2020-04-02 18:03:26 +02:00
Raphael Michel
c7dd50de0d CartMixin: Prevent None values in sorting function 2020-04-02 16:53:17 +02:00
Raphael Michel
a1caa65776 Revert "Upgrade jQuery version (but keep old one around for now for plugins)"
We've got to many instances of this around:
https://stackoverflow.com/questions/38871753/uncaught-typeerror-a-indexof-is-not-a-function-error-when-opening-new-foundat

This reverts commit cc46d55f5e.
2020-04-02 16:42:54 +02:00
Raphael Michel
260973345d Merge pull request #1634 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-02 14:41:19 +02:00
Raphael Michel
2c9b2620ea Add company and address fields to attendees (#1633)
* Add company and address fields to attendees

* Update src/pretix/control/templates/pretixcontrol/event/settings.html

Co-Authored-By: Martin Gross <gross@rami.io>

Co-authored-by: Martin Gross <gross@rami.io>
2020-04-02 14:41:09 +02:00
Maarten van den Berg
909c80e710 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate
2020-04-02 09:51:33 +02:00
Maarten van den Berg
5a218ae6a9 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3590 of 3590 strings)

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

powered by weblate
2020-04-02 09:51:33 +02:00
Raphael Michel
b498d45621 Pass gift_cards to order_fee_calculation 2020-04-02 09:50:44 +02:00
Raphael Michel
b02196434b Re-compute fees after applying a giftcard 2020-04-01 17:09:58 +02:00
Raphael Michel
c0edce7760 AJAX: Do not throw error on "abort" in Safari 2020-04-01 16:06:46 +02:00
Raphael Michel
cc46d55f5e Upgrade jQuery version (but keep old one around for now for plugins) 2020-04-01 16:06:46 +02:00
Martin Gross
ea8abb8dab Add Giftcard Redemption Export 2020-04-01 15:46:40 +02:00
Raphael Michel
f765d094b4 Fix crash in UCBrowser (PRETIXEU-20M) 2020-04-01 11:14:30 +02:00
Raphael Michel
86f222870d Cancelling orders: Do not send email to organizer if the refund is in transit 2020-03-31 17:56:51 +02:00
Martin Gross
19b5270d76 Fix tests (Caused by 61a1368ed2) 2020-03-31 14:34:10 +02:00
Martin Gross
db76b9b0ef Tiny fix to make Edge happy and display cancellation-slider 2020-03-31 14:04:57 +02:00
pretix translation bot
d23e53873f Translations update from Weblate (#1632)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

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

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

Co-authored-by: Martin Gross <martin@pc-coholic.de>
2020-03-31 13:35:14 +02:00
pretix translation bot
c116a4b998 Translations update from Weblate (#1631)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

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

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

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

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 99.4% (3569 of 3589 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate

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

Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-03-31 13:28:34 +02:00
Martin Gross
2471d4bca5 Update po files
[CI skip]

Signed-off-by: Martin Gross <gross@rami.io>
2020-03-31 12:47:09 +02:00
Martin Gross
8e04dbdcca Change "we keep" to "The organizer keeps" to avoid confusion 2020-03-31 12:45:32 +02:00
Raphael Michel
0928358396 Fix > vs >= in gift card message 2020-03-29 13:53:58 +02:00
Raphael Michel
23f783c15c Force django-libsass version to be upgraded 2020-03-26 20:51:54 +01:00
Raphael Michel
edae96c84f Fix TypeError during cancellation 2020-03-26 20:49:53 +01:00
Martin Gross
242ebdfae9 Show Subevent start time in select2-pickers (#1630)
* Add Subevent time to __str__

* Show subevent-dates in select2 picker

* Show event-dateblock (if enabled) on Widget Voucher redemption page

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

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

* Update src/pretix/control/templates/pretixcontrol/vouchers/index.html

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

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

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

* Remove date-block on non-subevent voucher redemption pages

Co-authored-by: Raphael Michel <michel@rami.io>
2020-03-26 13:10:45 +01:00
Raphael Michel
0ee502abec Improve performance by not re-evaluating main context processors on
every template rendering in a signal receiver
2020-03-26 09:50:58 +01:00
Raphael Michel
29cb1e93d8 Reduce number of queries on the order change form 2020-03-26 09:50:14 +01:00
Raphael Michel
c89242855c Reduce number of SQL queries on order detail page 2020-03-26 09:38:56 +01:00
Raphael Michel
61a1368ed2 Widget: Show date and time of subevent after calendar selection 2020-03-25 17:48:24 +01:00
Raphael Michel
ac3e00fa03 Color buttons green if they otherwise look like a headline 2020-03-25 17:48:09 +01:00
Raphael Michel
d9d0f7b6f3 Add Order.cancellation_date (#1629)
* Add Order.cancellation_date

* Add tests
2020-03-25 16:37:34 +01:00
Raphael Michel
ad5e2df3be Merge pull request #1628 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-25 14:59:07 +01:00
Raphael Michel
ec34561815 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate
2020-03-25 14:58:32 +01:00
Raphael Michel
e1540b1648 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate
2020-03-25 14:58:31 +01:00
Raphael Michel
a6b265455d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-25 14:14:28 +01:00
Raphael Michel
8a6334bd86 Introduce cancellation requests (#1627)
* Allow to adjust the cancellation fee without JS

* Introduce cancellation requests

* ignore→delete

* Change a few things after Martin's review

* Add a few tests
2020-03-25 14:13:55 +01:00
Raphael Michel
173a23722a Merge pull request #1625 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-25 14:12:26 +01:00
Raphael Michel
ab8eb2a34d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-25 13:03:51 +01:00
pajowu
30dcda616b Send checkin list mapping in event_copy_data signal (#1624)
* Send checkin list map in event_copy_data signal

* Add checkin_list_map to documentation and definition of event_copy_data
2020-03-25 13:03:45 +01:00
Raphael Michel
3eafec9d6e Allow customers to choose to receive their refund as a gift card (#1626)
* Minor text adjustments

* Allow users to receive their cancellation as a gift card
2020-03-25 11:41:40 +01:00
Raphael Michel
a5910016fd Allow users to increase cancellation fees (#1622)
* Allow users to increase cancellation fees

* Fix typo
2020-03-25 10:11:29 +01:00
pajowu
0a49b93b26 Render remaining badges onto new page even if it doesn't fill t… (#1621) 2020-03-24 17:43:58 +01:00
Raphael Michel
7449bea836 Add request argument to order_info/position_info signals 2020-03-24 15:57:00 +01:00
Raphael Michel
0fc4478332 Add docs on pretix-digital 2020-03-24 15:56:51 +01:00
Raphael Michel
0df4a6e7ed Add signals order_info_top and position_info_top 2020-03-24 14:22:02 +01:00
Benjamin Hättasch
a37cd380c8 Fix sample service config (#1620) 2020-03-24 09:10:56 +01:00
Raphael Michel
11b2bd8887 Allow markdown in multiple-choice question options 2020-03-23 17:55:52 +01:00
Raphael Michel
8986db0975 Merge pull request #1619 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-23 17:55:50 +01:00
Raphael Michel
2921611cb1 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-23 17:55:20 +01:00
Raphael Michel
785fb29513 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-23 17:55:19 +01:00
Raphael Michel
81c3d7fa17 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-23 17:02:11 +01:00
Raphael Michel
8ff963698d Text change "Modify" → "Add or remove tickets" on review order page 2020-03-23 16:47:25 +01:00
Raphael Michel
6da63e0169 Question form: Guess default country 2020-03-23 16:44:37 +01:00
Raphael Michel
f84903ae27 Drop removed context attribute from from_db_value 2020-03-23 16:44:37 +01:00
Raphael Michel
a0a7859b33 Update redis 2020-03-23 16:03:19 +01:00
Raphael Michel
af23d6e4bf Upgrade to Django 3.0 and other dependencies (#1568)
* Upgrade Django to 3.0 and other dependencies to recent versions

* Fix otp version contsraint

* Remove six dependency

* Resolve some warnings

* Fix failing tests

* Update django-countries

* Resolve all RemovedInDjango31Warnings in test suite

* Run isort

* Fix import

* Update PostgreSQL version on travis
2020-03-23 15:02:20 +01:00
Raphael Michel
7e9c9beace Allow to use a custom domain per event (#1617)
* Drop support for maindomain_urls/subdomain_urls in plugins

* Allow to use a custom domain per event

* Fix bug when manually saving domains

* Fix custom domains in debugging

* Fix middleware

* Fix middleware again, update docs
2020-03-23 13:03:14 +01:00
Raphael Michel
ac2fc2de5c Cancelling events: Allow to cancel all dates in a series 2020-03-23 10:37:10 +01:00
Raphael Michel
45e548873e Do not crash event page on social image generation 2020-03-22 11:04:51 +01:00
Raphael Michel
f484eb65df PayPal: Do not crash on failed refund 2020-03-22 11:04:51 +01:00
Raphael Michel
027a785ab5 Log out other sessions after email or 2FA changes 2020-03-22 11:04:51 +01:00
Raphael Michel
25b80cbb57 Fix invalid requirement spec 2020-03-20 12:46:15 +01:00
Raphael Michel
589fa0f9de Cancelling events: Send email even if refund failed 2020-03-20 12:38:57 +01:00
Raphael Michel
6d2989d15a Run a forked version of vat_moss 2020-03-20 11:06:19 +01:00
Raphael Michel
5bb27b29ae Seat statistics: Only use active variations 2020-03-16 16:32:34 +01:00
Raphael Michel
d17f8a71e6 Fix crashes in new statistical feature 2020-03-16 16:12:48 +01:00
Raphael Michel
b664cc712a Cancelling events: Allow to create manual and partial refunds 2020-03-16 16:00:44 +01:00
Raphael Michel
d61e8a9204 Cancelling events: Allow to select fee types to keep 2020-03-16 15:44:37 +01:00
Martin Gross
f00012a63e Statistics on sold and unsold seats, as well as potential profi… (#1613)
* Statistics on sold and unsold seats, as well as potential profits

* Rework of seats-stats

* Fix crash when all seats are assigned

* Update src/pretix/plugins/statistics/views.py

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

* Update src/pretix/plugins/statistics/views.py

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

* Update src/pretix/plugins/statistics/views.py

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

* Fix count of sold seats

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
2020-03-16 15:20:30 +01:00
Raphael Michel
bd238f76ce Fix broken test 2020-03-16 13:29:12 +01:00
Raphael Michel
703ae97820 Fix typo in German translation 2020-03-16 13:27:33 +01:00
Raphael Michel
1a60c5ea64 Fix missing bleach call in invoice renderer 2020-03-16 11:44:31 +01:00
Raphael Michel
1d3ac5f02f Fix StaticDevice.MultipleObjectsReturned
PRETIXEU-1YX
2020-03-16 11:41:33 +01:00
Raphael Michel
8d23d75dfd Only send download reminders if there's actually a download 2020-03-16 09:26:11 +01:00
Maico Timmerman
9a32668ee1 Make next url authentication backend dependent (#1609)
* Make next url authentication backend dependent

* Rename authentication next_url to get_next_url.

* Add test for custom authentication backend get_next_url.

* Fix typo in docstring of authentication backend get_next_url.
2020-03-15 11:05:57 +01:00
pajowu
ca0407a133 Subclass MultipleChoiceField to serialize to list (#1605)
* Subclass MultipleChoiceField to serialize to list

* Rename pretix.api.serializers.MultipleChoiceField to ListMultipleChoiceField

* Keep order in ListMultipleChoiceField
2020-03-14 22:04:12 +01:00
Raphael Michel
1de77b0784 Translations update from Weblate (#1612)
Translations update from Weblate
2020-03-14 22:02:09 +01:00
Maarten van den Berg
d0907d3dcf Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-13 08:00:11 +01:00
pajowu
81cc4bd768 Do not validate event creation form on back button (#1611) 2020-03-12 19:17:49 +01:00
pajowu
262639e063 Show checkbox if items per order limit is 1 (#1610) 2020-03-12 18:47:14 +01:00
Martin Gross
dedd93fb89 Add Support for Square POS payment_data 2020-03-12 14:33:37 +01:00
Raphael Michel
45f94aee03 Merge pull request #1602 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-12 13:31:40 +01:00
Maarten van den Berg
d36e7d033f Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-12 13:31:17 +01:00
Maarten van den Berg
b94bd277bf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-12 13:31:16 +01:00
Maico Timmerman
e5095185d9 Send widget_data in voucher redeem request. (#1606) 2020-03-12 13:31:12 +01:00
pajowu
d76ce47597 Allow creating question with dependency_question=None (#1607) 2020-03-12 13:30:18 +01:00
Martin Gross
58717850c2 Add date-on-frontpage check to secondary template 2020-03-12 12:06:26 +01:00
Martin Gross
29d52d4fe5 Second attempt at hiding date on frontpage in title 2020-03-12 12:00:18 +01:00
Martin Gross
34c9c40ddc Option to hide date-block on frontpage (#1603)
* Option to hide date-block on frontpage

* Also hide date in headline
2020-03-12 10:28:47 +01:00
Martin Gross
39d05a6c40 Duplicate generate_ticket for items on duplication 2020-03-11 18:44:29 +01:00
Martin Gross
b664222c62 Calculation fix for unlimited Quota w/ Waitinglist 2020-03-11 16:48:57 +01:00
Raphael Michel
1ee48a10b5 Allow to reactivate canceled orders (#1601) 2020-03-11 11:40:56 +01:00
Raphael Michel
2431a8b767 Fix typo 2020-03-09 17:37:13 +01:00
Raphael Michel
af84354e51 Resolve some warnings in the test suite 2020-03-09 14:57:19 +01:00
Raphael Michel
b04de880fc Bump to 3.8.0.dev0 2020-03-09 14:34:00 +01:00
Raphael Michel
11f3057f76 Bump version to 3.7.0 2020-03-09 14:33:17 +01:00
Raphael Michel
ba164c16f6 Fix test failure that only happens in the 20 days before daylight saving time changes 2020-03-09 12:56:58 +01:00
Raphael Michel
7ef766ddfa Cancelling events: Only show email fields conditionally 2020-03-09 11:44:25 +01:00
Raphael Michel
bcafcc7dd8 Fix invalid translation file 2020-03-09 11:36:59 +01:00
Raphael Michel
e5b7102abc Merge pull request #1600 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-09 11:21:33 +01:00
Raphael Michel
3601dd6bee Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-09 11:20:45 +01:00
Raphael Michel
a1d5854fbf Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-09 11:20:44 +01:00
Raphael Michel
09544a688d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-09 11:11:54 +01:00
Raphael Michel
58a5892cc0 Merge pull request #1598 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-09 11:11:12 +01:00
Maarten van den Berg
c9af76b46e Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.9% (3436 of 3510 strings)

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

powered by weblate
2020-03-09 01:00:13 +01:00
Maarten van den Berg
91753935cf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3510 of 3510 strings)

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

powered by weblate
2020-03-09 01:00:12 +01:00
323 changed files with 77559 additions and 54300 deletions

View File

@@ -13,24 +13,24 @@ services:
- postgresql
matrix:
include:
- python: 3.7
- python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.7
- python: 3.8
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7
- python: 3.8
env: JOB=style
- python: 3.7
- python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
- python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.8
env: JOB=doc-spelling
- python: 3.7
- python: 3.8
env: JOB=translation-spelling
addons:
postgresql: "9.4"
postgresql: "10"
mariadb: '10.3'
apt:
packages:

View File

@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
--sysctl net.core.somaxconn=4096
--sysctl net.core.somaxconn=4096 \
pretix/standalone:stable all
ExecStop=/usr/bin/docker stop %n

View File

@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones.
Requirements
@@ -133,7 +133,7 @@ command if you're running MySQL::
(venv)$ pip3 install "pretix[postgres]" gunicorn
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::

View File

@@ -151,6 +151,10 @@ last_modified datetime Last modificati
The ``order.fees.canceled`` attribute has been added.
.. versionchanged:: 3.8
The ``reactivate`` operation has been added.
.. _order-position-resource:
@@ -173,6 +177,13 @@ price money (string) Price of this p
attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
attendee_email string Specified attendee email address for this position (or ``null``)
company string Attendee company name (or ``null``)
street string Attendee street (or ``null``)
zipcode string Attendee ZIP code (or ``null``)
city string Attendee city (or ``null``)
country string Attendee country code (or ``null``)
state string Attendee state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position
@@ -236,6 +247,10 @@ pdf_data object Data object req
The attribute ``canceled`` has been added.
.. versionchanged:: 3.8
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
.. _order-payment-resource:
Order payment resource
@@ -380,6 +395,12 @@ List of all orders
"full_name": "Peter",
},
"attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null,
"tax_rate": "0.00",
"tax_value": "0.00",
@@ -536,6 +557,12 @@ Fetching individual orders
"full_name": "Peter",
},
"attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
@@ -816,9 +843,9 @@ Creating orders
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
creation.
* ``email``
* ``email`` (optional)
* ``locale``
* ``sales_channel``
* ``sales_channel`` (optional)
* ``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. This field is optional when the order status is ``"n"`` or the order total is
@@ -851,15 +878,21 @@ Creating orders
* ``positionid`` (optional, see below)
* ``item``
* ``variation``
* ``variation`` (optional)
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts``
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email``
* ``attendee_email`` (optional)
* ``company`` (optional)
* ``street`` (optional)
* ``zipcode`` (optional)
* ``city`` (optional)
* ``country`` (optional)
* ``state`` (optional)
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
* ``subevent``
* ``subevent`` (optional)
* ``answers``
* ``question``
@@ -1057,6 +1090,42 @@ Order state operations
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
still available.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ 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
{
"code": "ABC12",
"status": "n",
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to modify
:statuscode 200: no error
:statuscode 400: The order cannot be reactivated
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
Marks a paid order as unpaid.

View File

@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
from django.urls import resolve, reverse
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.control.signals import nav_event

View File

@@ -20,7 +20,7 @@ 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, order_gracefully_delete, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, 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
Check-ins
"""""""""
@@ -33,11 +33,11 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, 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
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
.. automodule:: pretix.presale.signals
:members: order_info, order_meta_from_request
:members: order_info, order_info_top, order_meta_from_request
Request flow
""""""""""""

View File

@@ -61,7 +61,7 @@ A working example would be::
from pretix.base.plugins import PluginConfig
except ImportError:
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class PaypalApp(PluginConfig):

View File

@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
implementation could look like::
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display
@receiver(signal=logentry_display)

277
doc/plugins/digital.rst Normal file
View File

@@ -0,0 +1,277 @@
Digital content
===============
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
such as live streams, videos, or material downloads.
Resource description
--------------------
The digital content resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal content ID
title multi-lingual string The content title (required)
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
url string The location of the digital content
description multi-lingual string A public description of the item. May contain Markdown
syntax and is not required.
available_from datetime The first date time at which this content will be shown
(or ``null``).
available_until datetime The last date time at which this content will b e shown
(or ``null``).
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
position integer An integer, used for sorting
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Returns a list of all digital content configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ 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,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Returns information on one content item, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the content to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Create a new digital content.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to create new content for
:param event: The ``slug`` field of the event to create new content for
:statuscode 201: no error
:statuscode 400: The content could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Update a content. 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.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"url": "https://mywebsite.com"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://mywebsite.com",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the content to modify
:statuscode 200: no error
:statuscode 400: The content could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Delete a digital content.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the content to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it

View File

@@ -15,3 +15,4 @@ If you want to **create** a plugin, please go to the
ticketoutputpdf
badges
campaigns
digital

View File

@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
With the pretix.eu hosted service
---------------------------------
Step 1: DNS Configuration
#########################
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
This page will show you instructions on how to set up your own domain. Basically, it works like this:
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
domain provider.
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
The value of the record should be ``www.pretix.eu``.
Step 2: Wait for the DNS entry to propagate
###########################################
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
Step 3: Tell us
###############
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
certificate for you (for free!) and configure the domain.
Now, tell us about your domain on the "Custom Domain" page to get started.
With a custom pretix installation
---------------------------------

View File

@@ -1 +1 @@
__version__ = "3.7.0.dev0"
__version__ = "3.8.0"

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from oauth2_provider.generators import (
generate_client_id, generate_client_secret,
)

View File

@@ -2,7 +2,7 @@ from datetime import timedelta
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy
from django.utils.translation import gettext_lazy
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else:
validated_data['seat'] = seat
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')

View File

@@ -1,4 +1,4 @@
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django_countries.serializers import CountryFieldMixin
from hierarkey.proxy import HierarkeyProxy
from pytz import common_timezones
@@ -532,6 +532,9 @@ class EventSettingsSerializer(serializers.Serializer):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
'show_items_outside_presale_period',
@@ -557,6 +560,10 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'confirm_text',
'order_email_asked_twice',
'payment_term_days',
@@ -610,6 +617,10 @@ class EventSettingsSerializer(serializers.Serializer):
'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
]
def __init__(self, *args, **kwargs):

View File

@@ -0,0 +1,29 @@
from collections import OrderedDict
from rest_framework import serializers
def remove_duplicates_from_list(data):
return list(OrderedDict.fromkeys(data))
class ListMultipleChoiceField(serializers.MultipleChoiceField):
def to_internal_value(self, data):
if isinstance(data, str) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
internal_value_data = [
super(serializers.MultipleChoiceField, self).to_internal_value(item)
for item in data
]
return remove_duplicates_from_list(internal_value_data)
def to_representation(self, value):
representation_data = [
self.choice_strings_to_values.get(str(item), item) for item in value
]
return remove_duplicates_from_list(representation_data)

View File

@@ -3,7 +3,7 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers.event import MetaDataField
@@ -287,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
if value:
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
if value == self.instance:
raise ValidationError('A question cannot depend on itself.')
if value == self.instance:
raise ValidationError('A question cannot depend on itself.')
return value
def validate(self, data):

View File

@@ -5,7 +5,7 @@ from decimal import Decimal
import pycountry
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy
from django.utils.translation import gettext_lazy
from django_countries.fields import Country
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -39,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
else:
elif hasattr(instance, 'country_old'):
return instance.country_old
@@ -211,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*')
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
@@ -516,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
country = CompatibleCountryField(source='*')
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for k, v in self.fields.items():
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
v.required = False
v.allow_blank = True
v.allow_null = True
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError(
@@ -576,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
@@ -862,7 +892,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
@@ -877,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
@@ -889,7 +919,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]

View File

@@ -1,7 +1,7 @@
from decimal import Decimal
from django.db.models import Q
from django.utils.translation import get_language, ugettext_lazy as _
from django.utils.translation import get_language, gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

View File

@@ -2,7 +2,7 @@ import logging
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.forms import AllowForm
from oauth2_provider.views import (

View File

@@ -9,7 +9,7 @@ from django.db.models.functions import Coalesce, Concat
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 _
from django.utils.translation import gettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, viewsets
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate
@@ -261,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def reactivate(self, request, **kwargs):
order = self.get_object()
if order.status != Order.STATUS_CANCELED:
return Response(
{'detail': 'The order is not allowed to be reactivated.'},
status=status.HTTP_400_BAD_REQUEST
)
try:
reactivate_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
)
except OrderError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)

View File

@@ -58,7 +58,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_organizer_settings'
def get_queryset(self):
return self.request.organizer.seating_plans.all()
return self.request.organizer.seating_plans.order_by('name')
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -195,7 +195,7 @@ class TeamViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_teams'
def get_queryset(self):
return self.request.organizer.teams.all()
return self.request.organizer.teams.order_by('pk')
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -268,7 +268,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.invites.all()
return self.team.invites.order_by('email')
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -305,7 +305,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.tokens.all()
return self.team.tokens.order_by('name')
def get_serializer_context(self):
ctx = super().get_serializer_context()

View File

@@ -7,7 +7,7 @@ import requests
from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled
from requests import RequestException
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.canceled',
_('Order canceled'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.reactivated',
_('Order reactivated'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.expired',
_('Order expired'),

View File

@@ -85,6 +85,16 @@ class BaseAuthBackend:
"""
return
def get_next_url(self, request):
"""
This method will be called after a successful login to determine the next URL. Pretix in general uses the
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
"""
if "next" in request.GET:
return request.GET.get("next")
return None
class NativeAuthBackend(BaseAuthBackend):
identifier = 'native'

View File

@@ -2,7 +2,7 @@ import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.signals import register_sales_channels

View File

@@ -9,7 +9,7 @@ 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 get_language, ugettext_lazy as _
from django.utils.translation import get_language, gettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber

View File

@@ -7,7 +7,7 @@ from typing import Tuple
from defusedcsv import csv
from django import forms
from django.utils.formats import localize
from django.utils.translation import ugettext, ugettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
@@ -180,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
]
for s, l in self.sheets:
choices += [
(s + ':default', str(l) + ' ' + ugettext('CSV (with commas)')),
(s + ':excel', str(l) + ' ' + ugettext('CSV (Excel-style)')),
(s + ':semicolon', str(l) + ' ' + ugettext('CSV (with semicolons)')),
(s + ':default', str(l) + ' ' + gettext('CSV (with commas)')),
(s + ':excel', str(l) + ' ' + gettext('CSV (Excel-style)')),
(s + ':semicolon', str(l) + ' ' + gettext('CSV (with semicolons)')),
]
ff = OrderedDict(
[

View File

@@ -5,7 +5,7 @@ from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import QuestionAnswer

View File

@@ -6,7 +6,7 @@ import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
from django.utils.translation import ugettext, ugettext_lazy
from django.utils.translation import gettext, gettext_lazy
from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
payments.append({
'PTID': '5',
'PTN': 'Lastschrift',
'PTNo4': ugettext('Event ticket {event}-{code}').format(
'PTNo4': gettext('Event ticket {event}-{code}').format(
event=self.event.slug.upper(),
code=invoice.order.code
),
@@ -199,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
[
('date_from',
forms.DateField(
label=ugettext_lazy('Start date'),
label=gettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=ugettext_lazy('End date'),
label=gettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
]
)

View File

@@ -7,7 +7,7 @@ import dateutil.parser
from django import forms
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPayment

View File

@@ -2,7 +2,7 @@ from collections import OrderedDict
from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPosition

View File

@@ -8,10 +8,10 @@ from django.db.models import (
)
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -22,7 +22,7 @@ from ..signals import register_data_exporters
class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist'
verbose_name = ugettext_lazy('Order data')
verbose_name = gettext_lazy('Order data')
@property
def sheets(self):
@@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
_('Attendee email'),
_('Company'),
_('Address'),
_('ZIP code'),
_('City'),
_('Country'),
pgettext('address', 'State'),
_('Voucher'),
_('Pseudonymization ID'),
]
@@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
)
row += [
op.attendee_email,
op.company or '',
op.street or '',
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state or '',
op.voucher.code if op.voucher else '',
op.pseudonymization_id,
]
@@ -414,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = ugettext_lazy('Order payments and refunds')
verbose_name = gettext_lazy('Order payments and refunds')
@property
def additional_form_fields(self):
@@ -485,7 +497,7 @@ class PaymentListExporter(ListExporter):
class QuotaListExporter(ListExporter):
identifier = 'quotalist'
verbose_name = ugettext_lazy('Quota availabilities')
verbose_name = gettext_lazy('Quota availabilities')
def iterate_list(self, form_data):
headers = [
@@ -514,7 +526,7 @@ class QuotaListExporter(ListExporter):
class InvoiceDataExporter(MultiSheetListExporter):
identifier = 'invoicedata'
verbose_name = ugettext_lazy('Invoice data')
verbose_name = gettext_lazy('Invoice data')
@property
def sheets(self):
@@ -698,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
return '{}_invoices'.format(self.event.slug)
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Giftcard Redemptions')
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
payments = OrderPayment.objects.filter(
order__event=self.event,
provider='giftcard'
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
provider='giftcard'
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
headers = [
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
]
yield headers
for obj in objs:
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
gc.secret,
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
gc.issuer
]
yield row
def get_filename(self):
return '{}_giftcardredemptions'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@@ -716,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
def register_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter

View File

@@ -3,7 +3,6 @@ import logging
import i18nfield.forms
from django import forms
from django.forms.models import ModelFormMetaclass
from django.utils import six
from django.utils.crypto import get_random_string
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
@@ -25,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
super().__init__(*args, **kwargs)
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
pass

View File

@@ -3,9 +3,10 @@ from django.conf import settings
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
class LoginForm(forms.Form):
@@ -36,7 +37,7 @@ class LoginForm(forms.Form):
if not settings.PRETIX_LONG_SESSIONS or backend.url:
del self.fields['keep_logged_in']
else:
self.fields.move_to_end('keep_logged_in')
move_to_end(self.fields, 'keep_logged_in')
def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):

View File

@@ -18,7 +18,7 @@ from django.forms import Select
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import (
get_language, pgettext_lazy, ugettext_lazy as _,
get_language, gettext_lazy as _, pgettext_lazy,
)
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -41,6 +41,7 @@ from pretix.base.settings import (
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.countries import CachedCountries
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -214,6 +215,10 @@ def guess_country(event):
return country
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -241,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required,
required=event.settings.attendee_names_required and not self.all_optional,
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Attendee name'),
@@ -249,7 +254,7 @@ class BaseQuestionsForm(forms.Form):
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
widget=forms.EmailInput(
@@ -258,6 +263,75 @@ class BaseQuestionsForm(forms.Form):
}
)
)
if item.admission and event.settings.attendee_company_asked:
self.fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.admission and event.settings.attendee_addresses_asked:
self.fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),
widget=forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
}),
initial=(cartpos.street if cartpos else orderpos.street),
)
self.fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
widget=forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
)
self.fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('City'),
initial=(cartpos.city if cartpos else orderpos.city),
widget=forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
self.fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Country'),
initial=country,
widget=forms.Select(attrs={
'autocomplete': 'country',
}),
)
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
cc = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif country:
cc = str(country)
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),
)
self.fields['state'].widget.is_required = True
for q in questions:
# Do we already have an answer? Provide it as the initial value
@@ -309,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField().formfield(
field = CountryField(
countries=CachedCountries
).formfield(
label=label, required=required,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.answer if initial else None,
initial=initial.answer if initial else guess_country(event),
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
@@ -332,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
help_text=help_text,
to_field_name='identifier',
widget=forms.CheckboxSelectMultiple,
widget=QuestionCheckboxSelectMultiple,
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
@@ -419,6 +495,10 @@ class BaseQuestionsForm(forms.Form):
def clean(self):
d = super().clean()
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
def question_is_visible(parentid, qvals):
@@ -500,6 +580,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
self.fields['country'].choices = CachedCountries()
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
cc = None

View File

@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User

View File

@@ -2,7 +2,7 @@ import re
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString

View File

@@ -4,7 +4,7 @@ from django import forms
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class DatePickerWidget(forms.DateInput):

View File

@@ -3,7 +3,7 @@ from contextlib import contextmanager
from django.conf import settings
from django.utils import translation
from django.utils.formats import date_format, number_format
from django.utils.translation import ugettext
from django.utils.translation import gettext
from i18nfield.fields import ( # noqa
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
)
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
def __str__(self):
if self.msgargs:
return ugettext(self.msg) % self.msgargs
return gettext(self.msg) % self.msgargs
else:
return ugettext(self.msg)
return gettext(self.msg)

View File

@@ -10,7 +10,7 @@ from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import (
get_language, pgettext, ugettext, ugettext_lazy,
get_language, gettext, gettext_lazy, pgettext,
)
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
@@ -687,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm

View File

@@ -15,7 +15,9 @@ from django.utils.translation.trans_real import (
)
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import get_domain
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
_supported = None
@@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer:
domain = get_domain(request.organizer)
if hasattr(request, 'event') and request.event:
domain = get_event_domain(request.event, fallback=True)
else:
domain = get_organizer_domain(request.organizer)
if domain:
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):

View File

@@ -6,7 +6,7 @@ import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
import pretix.base.validators
from pretix.base.i18n import language

View File

@@ -7,7 +7,7 @@ from django.db import migrations, models
from django.db.models import F
from django.db.models.functions import Concat
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
import pretix.base.models.auth
import pretix.base.validators

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.9 on 2020-03-21 15:12
from django.db import migrations, models
import pretix.base.models.auth
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0146_giftcardtransaction_text'),
]
operations = [
migrations.AddField(
model_name='user',
name='session_token',
field=models.CharField(default=pretix.base.models.auth.generate_session_token, max_length=32),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.4 on 2020-03-25 10:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0147_user_session_token'),
]
operations = [
migrations.CreateModel(
name='CancellationRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('cancellation_fee', models.DecimalField(decimal_places=2, max_digits=10)),
('refund_as_giftcard', models.BooleanField(default=False)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cancellation_requests', to='pretixbase.Order')),
],
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 3.0.4 on 2020-03-25 14:40
from django.db import migrations, models
from django.db.models import Count, OuterRef, Q, Subquery
from django.utils.timezone import now
def fill_cancellation_date(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
LogEntry = apps.get_model('pretixbase', 'LogEntry')
OrderPosition = apps.get_model('pretixbase', 'OrderPosition')
s = OrderPosition.all.filter(
order=OuterRef('pk'),
canceled=False,
).order_by().values('order').annotate(k=Count('id')).values('k')
for o in Order.objects.annotate(
pcnt=Subquery(s)
).filter(
Q(pcnt=0) | Q(pcnt__isnull=True) | Q(status="c")
).values('id').iterator():
le = LogEntry.objects.filter(
content_type__model="order",
object_id=o['id'],
action_type='pretix.event.order.canceled'
).order_by('-datetime').only('datetime').first()
if le:
Order.objects.filter(pk=o['id']).update(
cancellation_date=le.datetime,
last_modified=now()
)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0148_cancellationrequest'),
]
operations = [
migrations.AddField(
model_name='order',
name='cancellation_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.RunPython(fill_cancellation_date, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,74 @@
# Generated by Django 3.0.4 on 2020-04-01 11:24
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0149_order_cancellation_date'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='city',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='company',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='cartposition',
name='state',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='cartposition',
name='zipcode',
field=models.CharField(max_length=30, null=True),
),
migrations.AddField(
model_name='orderposition',
name='city',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='company',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='orderposition',
name='state',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='orderposition',
name='zipcode',
field=models.CharField(max_length=30, null=True),
),
]

View File

@@ -12,9 +12,9 @@ from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from u2flib_server.utils import (
@@ -54,6 +54,10 @@ def generate_notifications_token():
return get_random_string(length=32)
def generate_session_token():
return get_random_string(length=32)
class SuperuserPermissionSet:
def __contains__(self, item):
return True
@@ -110,6 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
)
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
auth_backend = models.CharField(max_length=255, default='native')
session_token = models.CharField(max_length=32, default=generate_session_token)
objects = UserManager()
@@ -382,6 +387,20 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
self._staff_session_cache[session_key] = sess
return self._staff_session_cache[session_key]
def get_session_auth_hash(self):
"""
Return an HMAC that needs to
"""
key_salt = "pretix.base.models.User.get_session_auth_hash"
payload = self.password
payload += self.email
payload += self.session_token
return salted_hmac(key_salt, payload).hexdigest()
def update_session_token(self):
self.session_token = generate_session_token()
self.save(update_fields=['session_token'])
class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT)

View File

@@ -1,7 +1,7 @@
from django.db import models
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.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel

View File

@@ -3,7 +3,7 @@ import string
from django.db import models
from django.db.models import Max
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel

View File

@@ -15,9 +15,10 @@ from django.db import models
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.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
@@ -293,7 +294,7 @@ class Event(EventMixin, LoggedModel):
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBanlistValidator()
@@ -370,6 +371,8 @@ class Event(EventMixin, LoggedModel):
"""
self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
@property
def social_image(self):
@@ -385,7 +388,7 @@ class Event(EventMixin, LoggedModel):
if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def free_seats(self, ignore_voucher=None, sales_channel='web'):
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
@@ -416,7 +419,7 @@ class Event(EventMixin, LoggedModel):
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@@ -622,8 +625,10 @@ class Event(EventMixin, LoggedModel):
q.dependency_question = question_map[q.dependency_question_id]
q.save(update_fields=['dependency_question'])
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all())
checkin_list_map[cl.pk] = cl
cl.pk = None
cl.event = self
cl.save()
@@ -678,7 +683,7 @@ class Event(EventMixin, LoggedModel):
event_copy_data.send(
sender=self, other=other,
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
question_map=question_map
question_map=question_map, checkin_list_map=checkin_list_map
)
def get_payment_providers(self, cached=False) -> dict:
@@ -1030,9 +1035,13 @@ class SubEvent(EventMixin, LoggedModel):
ordering = ("date_from", "name")
def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display())
return '{} - {} {}'.format(
self.name,
self.get_date_range_display(),
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
).strip()
def free_seats(self, ignore_voucher=None, sales_channel='web'):
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
@@ -1066,7 +1075,7 @@ class SubEvent(EventMixin, LoggedModel):
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs

View File

@@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
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 django.utils.translation import gettext_lazy as _
from pretix.base.banlist import banned
from pretix.base.models import LoggedModel
@@ -83,6 +83,7 @@ class GiftCard(LoggedModel):
class Meta:
unique_together = (('secret', 'issuer'),)
ordering = ("issuance",)
class GiftCardTransaction(models.Model):

View File

@@ -157,9 +157,12 @@ class Invoice(models.Model):
state_name = self.invoice_to_state
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
).name
try:
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
).name
except:
pass
parts = [
self.invoice_to_company,

View File

@@ -16,7 +16,7 @@ from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
@@ -455,7 +455,8 @@ class Item(LoggedModel):
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
t = self.tax_rule.tax(price, base_price_is=base_price_is,
currency=currency or self.event.currency)
if include_bundled:
for b in self.bundles.all():
@@ -1116,10 +1117,13 @@ class Question(LoggedModel):
return None
if self.type == Question.TYPE_CHOICE:
try:
return self.options.get(Q(pk=answer) | Q(identifier=answer))
except:
q = Q(identifier=answer)
if isinstance(answer, int) or answer.isdigit():
q |= Q(pk=answer)
o = self.options.filter(q).first()
if not o:
raise ValidationError(_('Invalid option selected.'))
return o
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
if isinstance(answer, str):
l_ = list(self.options.filter(

View File

@@ -6,7 +6,7 @@ from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import logentry_object_link

View File

@@ -1,5 +1,5 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class NotificationSetting(models.Model):

View File

@@ -4,6 +4,7 @@ import json
import logging
import os
import string
from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Union
@@ -25,7 +26,7 @@ from django.utils.encoding import escape_uri_path
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
@@ -43,6 +44,7 @@ from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -151,6 +153,9 @@ class Order(LockModel, LoggedModel):
datetime = models.DateTimeField(
verbose_name=_("Date"), db_index=True
)
cancellation_date = models.DateTimeField(
null=True, blank=True
)
expires = models.DateTimeField(
verbose_name=_("Expiration date")
)
@@ -469,6 +474,8 @@ class Order(LockModel, LoggedModel):
"""
from .checkin import Checkin
if self.cancellation_requests.exists():
return False
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
@@ -694,16 +701,19 @@ class Order(LockModel, LoggedModel):
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]:
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
check_voucher_usage=False) -> Union[bool, str]:
error_messages = {
'unavailable': _('The ordered product "{item}" is no longer available.'),
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
}
now_dt = now_dt or now()
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
quota_cache = {}
v_budget = {}
v_usage = Counter()
try:
for i, op in enumerate(positions):
if op.seat:
@@ -722,6 +732,13 @@ class Order(LockModel, LoggedModel):
))
v_budget[op.voucher] -= disc
if op.voucher and check_voucher_usage:
v_usage[op.voucher.pk] += 1
if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages:
raise Quota.QuotaExceededException(error_messages['voucher_usages'].format(
voucher=op.voucher.code
))
quotas = list(op.quotas)
if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
@@ -1049,6 +1066,13 @@ class AbstractPosition(models.Model):
'Seat', null=True, blank=True, on_delete=models.PROTECT
)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
class Meta:
abstract = True
@@ -2090,7 +2114,8 @@ class InvoiceAddress(models.Model):
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
@@ -2197,6 +2222,13 @@ class CachedCombinedTicket(models.Model):
created = models.DateTimeField(auto_now_add=True)
class CancellationRequest(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
created = models.DateTimeField(auto_now_add=True)
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
refund_as_giftcard = models.BooleanField(default=False)
@receiver(post_delete, sender=CachedTicket)
def cachedticket_delete(sender, instance, **kwargs):
if instance.file:

View File

@@ -5,7 +5,7 @@ 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 django.utils.translation import gettext_lazy as _
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator

View File

@@ -8,7 +8,7 @@ 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 _
from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.fields import I18nCharField

View File

@@ -8,7 +8,7 @@ from django.db.models import F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from pretix.base.email import get_email_context

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 pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
@@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs):
_('Order canceled'),
_('Order {order.code} has been canceled.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.reactivated',
_('Order reactivated'),
_('Order {order.code} has been reactivated.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.expired',

View File

@@ -325,7 +325,7 @@ class InvoiceAddressState(ImportColumn):
@property
def verbose_name(self):
return _('Invoice address') + ': ' + _('State')
return _('Invoice address') + ': ' + pgettext('address', 'State')
def clean(self, value, previous_values):
if value:
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
position.attendee_email = value
class AttendeeCompany(ImportColumn):
identifier = 'attendee_company'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Company')
def assign(self, value, order, position, invoice_address, **kwargs):
position.company = value or ''
class AttendeeStreet(ImportColumn):
identifier = 'attendee_street'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Address')
def assign(self, value, order, position, invoice_address, **kwargs):
position.address = value or ''
class AttendeeZip(ImportColumn):
identifier = 'attendee_zipcode'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('ZIP code')
def assign(self, value, order, position, invoice_address, **kwargs):
position.zipcode = value or ''
class AttendeeCity(ImportColumn):
identifier = 'attendee_city'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('City')
def assign(self, value, order, position, invoice_address, **kwargs):
position.city = value or ''
class AttendeeCountry(ImportColumn):
identifier = 'attendee_country'
default_value = None
@property
def initial(self):
return 'static:' + str(guess_country(self.event))
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Country')
def static_choices(self):
return list(countries)
def clean(self, value, previous_values):
if value and not Country(value).numeric:
raise ValidationError(_("Please enter a valid country code."))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
position.country = value or ''
class AttendeeState(ImportColumn):
identifier = 'attendee_state'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('State')
def clean(self, value, previous_values):
if value:
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(_("States are not supported for this country."))
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
match = [
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
if s.type in types and (s.code[3:] == value or s.name == value)
]
if len(match) == 0:
raise ValidationError(_("Please enter a valid state."))
return match[0].code[3:]
def assign(self, value, order, position, invoice_address, **kwargs):
position.state = value or ''
class Price(ImportColumn):
identifier = 'price'
verbose_name = gettext_lazy('Price')
@@ -596,6 +689,12 @@ def get_all_columns(event):
default.append(AttendeeNamePart(event, n, l))
default += [
AttendeeEmail(event),
AttendeeCompany(event),
AttendeeStreet(event),
AttendeeZip(event),
AttendeeCity(event),
AttendeeCountry(event),
AttendeeState(event),
Price(event),
Secret(event),
Locale(event),

View File

@@ -17,7 +17,7 @@ from django.http import HttpRequest
from django.template.loader import get_template
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
@@ -29,6 +29,7 @@ from pretix.base.models import (
OrderRefund, Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.services.cart import get_fees
from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
@@ -1106,8 +1107,16 @@ class GiftCardPayment(BasePaymentProvider):
return
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
remainder = cart['total'] - gc.value
if remainder >= Decimal('0.00'):
total = sum(p.total for p in cart['positions'])
# Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
# applied.
fees = get_fees(
self.event, request, total, cart['invoice_address'], cs.get('payment'),
cart['raw']
)
total += sum([f.value for f in fees])
remainder = 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)
@@ -1210,6 +1219,7 @@ class GiftCardPayment(BasePaymentProvider):
)
refund.info_data = {
'gift_card': gc.pk,
'gift_card_code': gc.secret,
'transaction_id': trans.pk,
}
refund.done()

View File

@@ -17,7 +17,7 @@ from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from PyPDF2 import PdfFileReader
from pytz import timezone
from reportlab.graphics import renderPDF
@@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Sample city"),
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
}),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Addon 1\nAddon 2"),

View File

@@ -7,7 +7,7 @@ from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
BASE_CHOICES = (
@@ -324,7 +324,7 @@ class ModelRelativeDateTimeField(models.CharField):
return value.to_string()
return value
def from_db_value(self, value, expression, connection, context):
def from_db_value(self, value, expression, connection):
if value is None:
return None
return RelativeDateWrapper.from_string(value)

View File

@@ -2,17 +2,15 @@ import logging
from decimal import Decimal
from django.db import transaction
from django.db.models import (
Count, Exists, IntegerField, OuterRef, Subquery, Sum,
)
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
from i18nfield.strings import LazyI18nString
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 (
Event, InvoiceAddress, Order, OrderFee, OrderPosition, SubEvent, User,
WaitingListEntry,
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, TolerantDict, mail
@@ -85,10 +83,10 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
keep_fee_percentage: str, keep_fees: bool,
send: bool, send_subject: dict, send_message: dict,
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None,
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
user: int=None):
user: int=None, refund_as_giftcard: bool=False):
send_subject = LazyI18nString(send_subject)
send_message = LazyI18nString(send_message)
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
@@ -151,27 +149,30 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
for o in orders_to_cancel.only('id', 'total'):
try:
fee = Decimal('0.00')
fee_sum = Decimal('0.00')
keep_fee_objects = []
if keep_fees:
fee += o.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
OrderFee.FEE_TYPE_CANCELLATION)
).aggregate(
s=Sum('value')
)['s'] or 0
for f in o.fees.all():
if f.fee_type in keep_fees:
fee += f.value
keep_fee_objects.append(f)
fee_sum += f.value
if keep_fee_percentage:
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee)
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
if keep_fee_fixed:
fee += Decimal(keep_fee_fixed)
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee)
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
refund_amount = o.payment_refund_sum
if auto_refund:
_try_auto_refund(o.pk)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
try:
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
finally:
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
except LockTimeoutException:
logger.exception("Could not cancel order")
failed += 1
@@ -212,7 +213,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
refund_amount = o.payment_refund_sum - o.total
if auto_refund:
_try_auto_refund(o.pk)
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)

View File

@@ -9,7 +9,7 @@ from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext as _
from django.utils.translation import gettext as _, pgettext_lazy
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
@@ -213,7 +213,7 @@ class CartManager:
has_variations=Count('variations'),
).filter(
id__in=[i for i in item_ids if i and i not in self._items_cache]
)
).order_by()
})
self._variations_cache.update({
v.pk: v
@@ -221,7 +221,7 @@ class CartManager:
'quotas'
).select_related('item', 'item__event').filter(
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
)
).order_by()
})
def _check_max_cart_size(self):

View File

@@ -2,7 +2,7 @@ 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 django.utils.translation import gettext as _
from pretix.base.models import (
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,

View File

@@ -2,7 +2,7 @@ from typing import Any, Dict
from django.core.files.base import ContentFile
from django.utils.timezone import override
from django.utils.translation import ugettext
from django.utils.translation import gettext
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import CachedFile, Event, cachedfile_name
@@ -26,7 +26,7 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
d = ex.render(form_data)
if d is None:
raise ExportError(
ugettext('Your export did not contain any data.')
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))

View File

@@ -15,7 +15,7 @@ from django.dispatch import receiver
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _
from django.utils.translation import gettext as _, pgettext
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString

View File

@@ -20,7 +20,7 @@ from django.core.mail import (
)
from django.core.mail.message import SafeMIMEText
from django.template.loader import get_template
from django.utils.translation import pgettext, ugettext as _
from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
@@ -276,20 +276,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
cm = lambda: scopes_disabled() # noqa
with cm():
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
if inv.file:
try:
with language(inv.order.locale):
email.attach(
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
inv.file.file.read(),
'application/pdf'
)
except:
logger.exception('Could not attach invoice to email')
pass
if event:
if order:
try:
@@ -344,6 +330,21 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
if inv.file:
try:
with language(inv.order.locale):
email.attach(
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
inv.file.file.read(),
'application/pdf'
)
except:
logger.exception('Could not attach invoice to email')
pass
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
try:

View File

@@ -15,7 +15,7 @@ from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
@@ -94,6 +94,54 @@ def mark_order_paid(*args, **kwargs):
raise NotImplementedError("This method is no longer supported since pretix 1.17.")
def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None):
"""
Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not
enough quota.
"""
if order.status != Order.STATUS_CANCELED:
raise OrderError('The order was not canceled.')
with order.event.lock() as now_dt:
is_available = force or order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True)
if is_available is True:
if order.payment_refund_sum >= order.total:
order.status = Order.STATUS_PAID
else:
order.status = Order.STATUS_PENDING
order.cancellation_date = None
order.set_expires(now(),
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
with transaction.atomic():
order.save(update_fields=['expires', 'status', 'cancellation_date'])
order.log_action(
'pretix.event.order.reactivated',
user=user,
auth=auth,
data={
'expires': order.expires,
}
)
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
gc.transactions.create(value=position.price, order=order)
break
else:
raise OrderError(is_available)
order_approved.send(order.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(order.event, order=order)
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
"""
Extends the deadline of an order. If the order is already expired, the quota will be checked to
@@ -117,9 +165,10 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
'state_change': was_expired
}
)
if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices:
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
if order.status == Order.STATUS_PENDING:
@@ -271,12 +320,13 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None):
cancellation_fee=None, keep_fees=None):
"""
Mark this order as canceled
:param order: The order to change
:param user: The user that performed the change
"""
# If new actions are added to this function, make sure to add the reverse operation to reactivate_order()
with transaction.atomic():
if isinstance(order, int):
order = Order.objects.select_for_update().get(pk=order)
@@ -318,31 +368,38 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
position.save(update_fields=['canceled'])
new_fee = cancellation_fee
for fee in order.fees.all():
fee.canceled = True
fee.save(update_fields=['canceled'])
if keep_fees and fee in keep_fees:
new_fee -= fee.value
else:
fee.canceled = True
fee.save(update_fields=['canceled'])
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=cancellation_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
)
f._calculate_tax()
f.save()
if new_fee:
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=new_fee,
tax_rule=order.event.settings.tax_rate_default,
order=order,
)
f._calculate_tax()
f.save()
if order.payment_refund_sum < cancellation_fee:
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
order.status = Order.STATUS_PAID
order.total = f.value
order.save(update_fields=['status', 'total'])
order.total = cancellation_fee
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
if i:
invoices.append(generate_invoice(order))
else:
with order.event.lock():
order.status = Order.STATUS_CANCELED
order.save(update_fields=['status'])
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date'])
for position in order.positions.all():
if position.voucher:
@@ -350,6 +407,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
order.cancellation_requests.all().delete()
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
@@ -633,7 +691,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
total = sum([c.price for c in positions])
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
meta_info=meta_info, positions=positions):
meta_info=meta_info, positions=positions, gift_cards=gift_cards):
if resp:
fees += resp
total += sum(f.value for f in fees)
@@ -970,7 +1028,6 @@ def send_download_reminders(sender, **kwargs):
F('event__date_from')
)
).filter(
status=Order.STATUS_PAID,
download_reminder_sent=False,
datetime__lte=now() - timedelta(hours=2),
first_date__gte=today,
@@ -1000,6 +1057,22 @@ def send_download_reminders(sender, **kwargs):
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
continue
if not o.ticket_download_available:
continue
positions = o.positions.select_related('item')
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or not \
o.event.settings.ticket_download_pending:
continue
send = False
for p in positions:
if p.generate_ticket:
send = True
break
if not send:
continue
with language(o.locale):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
@@ -1017,6 +1090,9 @@ def send_download_reminders(sender, **kwargs):
if event.settings.mail_send_download_reminder_attendee:
for p in o.positions.all():
if not p.generate_ticket:
continue
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0
@@ -1148,6 +1224,27 @@ class OrderChangeManager:
self._quotadiff.subtract(position.quotas)
self._operations.append(self.SubeventOperation(position, subevent))
def change_item_and_subevent(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation],
subevent: SubEvent):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (variation.quotas.filter(subevent=subevent)
if variation else item.quotas.filter(subevent=subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._operations.append(self.ItemOperation(position, item, variation))
self._operations.append(self.SubeventOperation(position, subevent))
def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position))
@@ -1816,7 +1913,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
raise OrderError(str(error_messages['busy']))
def _try_auto_refund(order):
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
refund_as_giftcard=False):
notify_admin = False
error = False
if isinstance(order, int):
@@ -1825,14 +1923,55 @@ def _try_auto_refund(order):
if refund_amount <= Decimal('0.00'):
return
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund = sum(proposals.values()) == refund_amount
if refund_as_giftcard:
proposals = {}
can_auto_refund = True
can_auto_refund_sum = refund_amount
with transaction.atomic():
giftcard = order.event.organizer.issued_gift_cards.create(
currency=order.event.currency,
testmode=order.testmode
)
giftcard.log_action('pretix.giftcards.created', data={})
r = order.refunds.create(
order=order,
payment=None,
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
execution_date=now(),
amount=can_auto_refund_sum,
provider='giftcard',
info=json.dumps({
'gift_card': giftcard.pk
})
)
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
order.log_action('pretix.event.order.refund.failed', {
'local_id': r.local_id,
'provider': r.provider,
'error': str(e)
})
error = True
notify_admin = True
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
else:
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund_sum = sum(proposals.values())
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
if can_auto_refund:
for p, value in proposals.items():
with transaction.atomic():
r = order.refunds.create(
payment=p,
source=OrderRefund.REFUND_SOURCE_BUYER,
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
provider=p.provider
@@ -1856,10 +1995,24 @@ def _try_auto_refund(order):
error = True
notify_admin = True
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
notify_admin = True
elif refund_amount != Decimal('0.00'):
notify_admin = True
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
if manual_refund:
with transaction.atomic():
r = order.refunds.create(
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=refund_amount - can_auto_refund_sum,
provider='manual'
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
else:
notify_admin = True
if notify_admin:
order.log_action('pretix.event.order.refund.requested')
@@ -1874,13 +2027,13 @@ def _try_auto_refund(order):
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False):
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
try:
try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
if try_auto_refund:
_try_auto_refund(order)
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
return ret
except LockTimeoutException:
self.retry()

View File

@@ -1,5 +1,5 @@
from django.db.models import Count, Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import LazyLocaleException
from pretix.base.models import CartPosition, Seat
@@ -25,7 +25,7 @@ def validate_plan_change(event, subevent, plan):
subevent=subevent,
).filter(
Q(has_v=True) | Q(has_op=True)
).values_list('seat_guid', flat=True)
).values_list('seat_guid', flat=True).order_by()
)
new_seats = {
ss.guid for ss in plan.iter_all_seats()
@@ -40,7 +40,7 @@ def generate_seats(event, subevent, plan, mapping):
current_seats = {}
for s in event.seats.select_related('product').annotate(
has_op=Count('orderposition'), has_v=Count('vouchers')
).filter(subevent=subevent):
).filter(subevent=subevent).order_by():
if s.seat_guid in current_seats:
s.delete() # Duplicates should not exist
else:

View File

@@ -8,7 +8,7 @@ from dateutil.parser import parse
from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledEventTask

View File

@@ -6,7 +6,7 @@ from django.db.models import (
Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When,
)
from django.utils.timezone import make_aware
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent

View File

@@ -3,7 +3,7 @@ import os
from django.core.files.base import ContentFile
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from pretix.base.i18n import language

View File

@@ -5,7 +5,7 @@ from datetime import timedelta
import requests
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.utils.translation import gettext_lazy as _, gettext_noop
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
@@ -91,7 +91,7 @@ def send_update_notification_email():
gs.settings.update_check_email,
_('pretix update available'),
LazyI18nString.from_gettext(
ugettext_noop(
gettext_noop(
'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your '
'pretix installation. Please click on the following link for more information:\n\n {url} \n\n'
'You can always find information on the latest updates on the pretix.eu blog:\n\n'

View File

@@ -11,7 +11,7 @@ from django.core.files import File
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Model
from django.utils.translation import (
pgettext, pgettext_lazy, ugettext_lazy as _, ugettext_noop,
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
)
from django_countries import countries
from hierarkey.models import GlobalSettingsBase, Hierarkey
@@ -19,6 +19,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from rest_framework import serializers
from pretix.api.serializers.fields import ListMultipleChoiceField
from pretix.api.serializers.i18n import I18nField
from pretix.base.models.tax import TaxRule
from pretix.base.reldate import (
@@ -104,6 +105,44 @@ DEFAULTS = {
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
)
},
'attendee_company_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for company per ticket"),
)
},
'attendee_company_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require company per ticket"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
)
},
'attendee_addresses_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for postal addresses per ticket"),
)
},
'attendee_addresses_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require postal addresses per ticket"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
)
},
'order_email_asked_twice': {
'default': 'False',
'type': bool,
@@ -365,7 +404,7 @@ DEFAULTS = {
'type': RelativeDateWrapper,
'form_class': RelativeDateField,
'serializer_class': SerializerRelativeDateField,
'form_kawrgs': dict(
'form_kwargs': dict(
label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the number of "
"days configured above. If you use the event series feature and an order contains tickets for "
@@ -639,7 +678,7 @@ DEFAULTS = {
'locales': {
'default': json.dumps([settings.LANGUAGE_CODE]),
'type': list,
'serializer_class': serializers.MultipleChoiceField,
'serializer_class': ListMultipleChoiceField,
'serializer_kwargs': dict(
choices=settings.LANGUAGES,
required=True,
@@ -668,6 +707,17 @@ DEFAULTS = {
label=_("Default language"),
)
},
'show_dates_on_frontpage': {
'default': 'True',
'type': bool,
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"does however not affect the display in other locations."),
)
},
'show_date_to': {
'default': 'True',
'type': bool,
@@ -770,8 +820,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Use feature"),
help_text=_("Use pretix to generate tickets for the user to download and print out."),
label=_("Allow users to download tickets"),
help_text=_("If this is off, nobody can download a ticket."),
)
},
'ticket_download_date': {
@@ -792,7 +842,11 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Offer to download tickets separately for add-on products"),
label=_("Generate tickets for add-on products"),
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
'products. With this option, a separate ticket is issued for every add-on product as well.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
)
},
'ticket_download_nonadm': {
@@ -801,7 +855,11 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Generate tickets for non-admission products"),
label=_("Generate tickets for all products"),
help_text=_('If turned off, tickets are only issued for products that are marked as an "admission ticket"'
'in the product settings. You can also turn off tickt issuing in every product separately.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
)
},
'ticket_download_pending': {
@@ -810,7 +868,10 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Offer to download tickets even before an order is paid"),
label=_("Generate tickets for pending orders"),
help_text=_('If turned off, ticket downloads are only possible after an order has been marked as paid.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
)
},
'event_list_availability': {
@@ -895,6 +956,66 @@ DEFAULTS = {
label=_("Keep a percentual cancellation fee"),
)
},
'cancel_allow_user_paid_adjust_fees': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Allow customers to voluntarily choose a lower refund"),
help_text=_("With this option enabled, your customers can choose to get a smaller refund to support you.")
)
},
'cancel_allow_user_paid_adjust_fees_explanation': {
'default': LazyI18nString.from_gettext(gettext_noop(
'However, if you want us to help keep the lights on here, please consider using the slider below to '
'request a smaller refund. Thank you!'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voluntary lower refund explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
"which your customers can use to choose the amount they would like to receive. You can use it "
"e.g. to explain choosing a lower refund will help your organisation.")
)
},
'cancel_allow_user_paid_require_approval': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Customers can only request a cancellation that needs to be approved by the event organizer "
"before the order is canceled and a refund is issued."),
)
},
'cancel_allow_user_paid_refund_as_giftcard': {
'default': 'off',
'type': str,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=[
('off', _('All refunds are issued to the original payment method')),
('option', _('Customers can choose between a gift card and a refund to their payment method')),
('force', _('All refunds are issued as gift cards')),
],
),
'form_class': forms.ChoiceField,
'form_kwargs': dict(
label=_('Refund method'),
choices=[
('off', _('All refunds are issued to the original payment method')),
('option', _('Customers can choose between a gift card and a refund to their payment method')),
('force', _('All refunds are issued as gift cards')),
],
widget=forms.RadioSelect,
# When adding a new ordering, remember to also define it in the event model
)
},
'cancel_allow_user_paid_until': {
'default': None,
'type': RelativeDateWrapper,
@@ -994,7 +1115,7 @@ DEFAULTS = {
},
'mail_text_resend_link': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you receive this message because you asked us to send you the link
to your order for {event}.
@@ -1007,7 +1128,7 @@ Your {event} team"""))
},
'mail_text_resend_all_links': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
somebody requested a list of your orders for {event}.
The list is as follows:
@@ -1019,7 +1140,7 @@ Your {event} team"""))
},
'mail_text_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
you have been registered for {event} successfully.
@@ -1031,7 +1152,7 @@ Your {event} team"""))
},
'mail_text_order_free': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order for {event} was successful. As you only ordered free products,
no payment is required.
@@ -1048,7 +1169,7 @@ Your {event} team"""))
},
'mail_text_order_placed_require_approval': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your order for {event}. Since you ordered
a product that requires approval by the event organizer, we ask you to
@@ -1062,7 +1183,7 @@ Your {event} team"""))
},
'mail_text_order_placed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your order for {event} with a total value
of {total_with_currency}. Please complete your payment before {expire_date}.
@@ -1081,7 +1202,7 @@ Your {event} team"""))
},
'mail_text_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
a ticket for {event} has been ordered for you.
@@ -1093,7 +1214,7 @@ Your {event} team"""))
},
'mail_text_order_changed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order for {event} has been changed.
@@ -1105,7 +1226,7 @@ Your {event} team"""))
},
'mail_text_order_paid': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your payment for {event}. Thank you!
@@ -1123,7 +1244,7 @@ Your {event} team"""))
},
'mail_text_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
a ticket for {event} that has been ordered for you is now paid.
@@ -1139,7 +1260,7 @@ Your {event} team"""))
},
'mail_text_order_expire_warning': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we did not yet receive a full payment for your order for {event}.
Please keep in mind that we only guarantee your order if we receive
@@ -1153,7 +1274,7 @@ Your {event} team"""))
},
'mail_text_waiting_list': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you submitted yourself to the waiting list for {event},
for the product {product}.
@@ -1176,7 +1297,7 @@ Your {event} team"""))
},
'mail_text_order_canceled': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order {code} for {event} has been canceled.
@@ -1188,7 +1309,7 @@ Your {event} team"""))
},
'mail_text_order_approved': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we approved your order for {event} and will be happy to welcome you
at our event.
@@ -1204,7 +1325,7 @@ Your {event} team"""))
},
'mail_text_order_denied': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
unfortunately, we denied your order request for {event}.
@@ -1219,7 +1340,7 @@ Your {event} team"""))
},
'mail_text_order_custom_mail': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
You can change your order details and view the status of your order at
{url}
@@ -1237,7 +1358,7 @@ Your {event} team"""))
},
'mail_text_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
you are registered for {event}.
@@ -1249,7 +1370,7 @@ Your {event} team"""))
},
'mail_text_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you bought a ticket for {event}.
@@ -1365,6 +1486,32 @@ Your {event} team"""))
widget=I18nTextarea
)
},
'banner_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (top)"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above every page of your shop. Please only use this for "
"very important messages.")
)
},
'banner_text_bottom': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (bottom)"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown below every page of your shop. Please only use this for "
"very important messages.")
)
},
'voucher_explanation_text': {
'default': '',
'type': LazyI18nString,
@@ -1379,7 +1526,7 @@ Your {event} team"""))
)
},
'checkout_email_helptext': {
'default': LazyI18nString.from_gettext(ugettext_noop(
'default': LazyI18nString.from_gettext(gettext_noop(
'Make sure to enter a valid email address. We will send you an order '
'confirmation including a link that you need to access your order later.'
)),

View File

@@ -7,7 +7,7 @@ from django.db.models import Max, Q
from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.api.serializers.order import (
AnswerSerializer, InvoiceAddressSerializer,
@@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded'])
class AttendeeNameShredder(BaseDataShredder):
verbose_name = _('Attendee names')
identifier = 'attendee_names'
description = _('This will remove all attendee names from order positions, as well as logged changes to them.')
class AttendeeInfoShredder(BaseDataShredder):
verbose_name = _('Attendee info')
identifier = 'attendee_info'
description = _('This will remove all attendee names and postal addresses from order positions, as well as logged '
'changes to them.')
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'attendee-names.json', 'application/json', json.dumps({
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
for op in OrderPosition.all.filter(
yield 'attendee-info.json', 'application/json', json.dumps({
'{}-{}'.format(op.order.code, op.positionid): {
'name': op.attendee_name,
'company': op.company,
'street': op.street,
'zipcode': op.zipcode,
'city': op.city,
'country': str(op.country) if op.country else None,
'state': op.state
} for op in OrderPosition.all.filter(
order__event=self.event
).filter(
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
@@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder):
OrderPosition.all.filter(
order__event=self.event
).filter(
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
zipcode=None, city=None)
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data
@@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder):
d['data'][i]['attendee_name_parts'] = {
'_legacy': ''
}
if 'company' in row:
d['data'][i]['company'] = ''
if 'street' in row:
d['data'][i]['street'] = ''
if 'zipcode' in row:
d['data'][i]['zipcode'] = ''
if 'city' in row:
d['data'][i]['city'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
@@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder):
def register_payment_provider(sender, **kwargs):
return [
EmailAddressShredder,
AttendeeNameShredder,
AttendeeInfoShredder,
InvoiceAddressShredder,
QuestionAnswerShredder,
InvoiceShredder,

View File

@@ -341,6 +341,16 @@ as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_reactivated = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time a canceled order is reactivated. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_expired = EventPluginSignal(
providing_args=["order"]
)
@@ -472,7 +482,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
"""
event_copy_data = EventPluginSignal(
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map"]
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map", "checkin_list_map"]
)
"""
This signal is sent out when a new event is created as a clone of an existing event, i.e.
@@ -484,9 +494,9 @@ but you might need to modify that data.
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
keyword argument will contain the event to **copy from**. The keyword arguments
``tax_map``, ``category_map``, ``item_map``, ``question_map``, and ``variation_map`` contain
mappings from object IDs in the original event to objects in the new event of the respective
types.
``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``variation_map`` and
``checkin_list_map`` contain mappings from object IDs in the original event to objects
in the new event of the respective types.
"""
item_copy_data = EventPluginSignal(
@@ -517,7 +527,7 @@ an OrderedDict of (setting name, form field).
"""
order_fee_calculation = EventPluginSignal(
providing_args=['positions', 'invoice_address', 'meta_info', 'total']
providing_args=['positions', 'invoice_address', 'meta_info', 'total', 'gift_cards']
)
"""
This signals allows you to add fees to an order while it is being created. You are expected to
@@ -528,7 +538,8 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
keyword argument will contain the total cart sum without any fees. You should not rely on this
``total`` value for fee calculations as other fees might interfere.
``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists
the gift cards in use.
"""
order_fee_type_name = EventPluginSignal(

View File

@@ -0,0 +1,2 @@
{% load rich_text %}
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label|rich_text_snippet }}</label>{% endif %}

View File

@@ -7,7 +7,7 @@ from django import template
from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
register = template.Library()
@@ -66,7 +66,7 @@ ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/')
if not is_safe_url(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'

View File

@@ -6,7 +6,7 @@ from zipfile import ZipFile
from django import forms
from django.http import HttpRequest
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.settings import SettingsSandbox
@@ -142,7 +142,7 @@ class BaseTicketOutput:
return OrderedDict([
('_enabled',
forms.BooleanField(
label=_('Enable output'),
label=_('Enable ticket format'),
required=False,
)),
])

View File

@@ -133,7 +133,7 @@ def timeline_for_event(event, subevent=None):
if not event.has_subevents:
days = event.settings.get('mail_days_download_reminder', as_type=int)
if days is not None:
if days is not None and event.settings.ticket_download:
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
tl.append(TimelineEvent(
event=event, subevent=subevent,

View File

@@ -1,7 +1,7 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class BanlistValidator:

View File

@@ -5,7 +5,7 @@ from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id

View File

@@ -85,10 +85,20 @@ class BaseQuestionsViewMixin:
for k, v in form.cleaned_data.items():
if k == 'attendee_name_parts':
form.pos.attendee_name_parts = v if v else None
form.pos.save()
elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None
form.pos.save()
elif k == 'company':
form.pos.company = v if v != '' else None
elif k == 'street':
form.pos.street = v if v != '' else None
elif k == 'zipcode':
form.pos.zipcode = v if v != '' else None
elif k == 'city':
form.pos.city = v if v != '' else None
elif k == 'country':
form.pos.country = v if v != '' else None
elif k == 'state':
form.pos.state = v if v != '' else None
elif k.startswith('question_'):
field = form.fields[k]
if hasattr(field, 'answer'):
@@ -119,7 +129,7 @@ class BaseQuestionsViewMixin:
meta_info['question_form_data'][k] = v
form.pos.meta_info = json.dumps(meta_info)
form.pos.save(update_fields=['meta_info'])
form.pos.save()
return not failed
def _save_to_answer(self, field, answer, value):

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from pretix.celery_app import app

View File

@@ -16,6 +16,7 @@ from pretix.control.navigation import (
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale,
)
from ..multidomain.urlreverse import get_event_domain
from .signals import html_head, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -25,6 +26,12 @@ def contextprocessor(request):
"""
Adds data to all template contexts
"""
if not hasattr(request, '_pretix_control_default_context'):
request._pretix_control_default_context = _default_context(request)
return request._pretix_control_default_context
def _default_context(request):
try:
url = resolve(request.path_info)
except Resolver404:
@@ -51,7 +58,7 @@ def contextprocessor(request):
if request.event.settings.get('payment_term_weekdays'):
_js_payment_weekdays_disabled = '[0,6]'
ctx['has_domain'] = request.event.organizer.domains.exists()
ctx['has_domain'] = get_event_domain(request.event, fallback=True) is not None
if not request.event.testmode:
with scope(organizer=request.organizer):

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.forms.utils import from_current_timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from ...base.forms import I18nModelForm
# Import for backwards compatibility with okd import paths

View File

@@ -1,4 +1,4 @@
from urllib.parse import urlencode
from urllib.parse import urlencode, urlparse
from django import forms
from django.conf import settings
@@ -10,7 +10,7 @@ from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import (
@@ -32,6 +32,7 @@ from pretix.control.forms import (
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -291,6 +292,15 @@ class EventUpdateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.change_slug = kwargs.pop('change_slug', False)
self.domain = kwargs.pop('domain', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
if not self.change_slug:
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
@@ -298,6 +308,47 @@ class EventUpdateForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
if self.domain:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Custom domain'),
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def save(self, commit=True):
instance = super().save(commit)
if self.domain:
current_domain = instance.domains.first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif not current_domain:
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif current_domain:
current_domain.delete()
instance.cache.clear()
return instance
def clean_slug(self):
if self.change_slug:
@@ -439,6 +490,7 @@ class EventSettingsForm(SettingsForm):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
'show_items_outside_presale_period',
@@ -463,7 +515,13 @@ class EventSettingsForm(SettingsForm):
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_company_asked',
'attendee_company_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'confirm_text',
'banner_text',
'banner_text_bottom',
'order_email_asked_twice',
'last_order_modification_date',
]
@@ -513,6 +571,10 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
]
@@ -1188,7 +1250,7 @@ class QuickSetupForm(I18nForm):
class QuickSetupProductForm(I18nForm):
name = I18nFormField(
max_length=255,
max_length=200, # Max length of Quota.name
label=_("Product name"),
widget=I18nTextInput
)

View File

@@ -8,7 +8,7 @@ from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
@@ -220,6 +220,7 @@ class EventOrderFilterForm(OrderFilterForm):
('underpaid', _('Underpaid')),
('pendingpaid', _('Pending (but fully paid)')),
('testmode', _('Test mode')),
('rc', _('Cancellation requested')),
),
required=False,
)
@@ -305,6 +306,10 @@ class EventOrderFilterForm(OrderFilterForm):
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
)
elif fdata.get('status') == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
)
elif fdata.get('status') == 'pendingpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(

View File

@@ -1,7 +1,7 @@
from collections import OrderedDict
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from pretix.base.forms import SettingsForm

View File

@@ -7,7 +7,7 @@ from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
from django.utils.translation import (
pgettext_lazy, ugettext as __, ugettext_lazy as _,
gettext as __, gettext_lazy as _, pgettext_lazy,
)
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
@@ -303,6 +303,7 @@ class ItemCreateForm(I18nModelForm):
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
self.instance.generate_tickets = self.cleaned_data['copy_from'].generate_tickets
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
self.instance.free_price = self.cleaned_data['copy_from'].free_price
self.instance.original_price = self.cleaned_data['copy_from'].original_price

View File

@@ -8,7 +8,7 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import make_aware, now
from django.utils.translation import (
gettext_noop, pgettext_lazy, ugettext_lazy as _,
gettext_lazy as _, gettext_noop, pgettext_lazy,
)
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
@@ -16,7 +16,9 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
from pretix.base.models import (
InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
from pretix.control.forms.widgets import Select2
@@ -239,6 +241,7 @@ class OrderPositionAddForm(forms.Form):
)
def __init__(self, *args, **kwargs):
self.items = kwargs.pop('items')
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
@@ -248,11 +251,13 @@ class OrderPositionAddForm(forms.Form):
ia = None
choices = []
for i in order.event.items.prefetch_related('variations').all():
for i in self.items:
pname = str(i)
if not i.is_available():
pname += ' ({})'.format(_('inactive'))
variations = list(i.variations.all())
if i.tax_rule: # performance optimization
i.tax_rule.event = order.event
if variations:
for v in variations:
p = get_price(i, v, invoice_address=ia)
@@ -262,7 +267,11 @@ class OrderPositionAddForm(forms.Form):
p = get_price(i, invoice_address=ia)
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(order.event.currency))))
self.fields['itemvar'].choices = choices
if ItemAddOn.objects.filter(base_item__event=order.event).exists():
if order.event.cache.get_or_set(
'has_addon_products',
default=lambda: ItemAddOn.objects.filter(base_item__event=order.event).exists(),
timeout=300
):
self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related(
'item', 'variation'
)
@@ -291,10 +300,12 @@ class OrderPositionAddForm(forms.Form):
class OrderPositionAddFormset(forms.BaseFormSet):
def __init__(self, *args, **kwargs):
self.order = kwargs.pop('order', None)
self.items = kwargs.pop('items')
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['order'] = self.order
kwargs['items'] = self.items
return super()._construct_form(i, **kwargs)
@property
@@ -305,6 +316,7 @@ class OrderPositionAddFormset(forms.BaseFormSet):
empty_permitted=True,
use_required_attribute=False,
order=self.order,
items=self.items,
)
self.add_fields(form, None)
return form
@@ -344,6 +356,7 @@ class OrderPositionChangeForm(forms.Form):
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')
items = kwargs.pop('items')
initial = kwargs.get('initial', {})
initial['price'] = instance.price
@@ -372,7 +385,7 @@ class OrderPositionChangeForm(forms.Form):
choices = [
('', _('(Unchanged)'))
]
for i in instance.order.event.items.prefetch_related('variations').all():
for i in items:
pname = str(i)
if not i.is_available():
pname += ' ({})'.format(_('inactive'))
@@ -524,14 +537,29 @@ class EventCancelForm(forms.Form):
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'Date'),
required=True,
empty_label=None
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
auto_refund = forms.BooleanField(
label=_('Automatically refund money if possible'),
initial=True,
required=False
)
manual_refund = forms.BooleanField(
label=_('Create manual refund if the payment method odes not support automatic refunds'),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
initial=True,
required=False,
help_text=_('If checked, all payments with a payment method not supporting automatic refunds will be on your '
'manual refund to-do list. Do not check if you want to refund some of the orders by offsetting '
'with different orders or issuing gift cards.')
)
refund_as_giftcard = forms.BooleanField(
label=_('Refund order value to a gift card instead instead of the original payment method'),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
initial=False,
required=False,
)
keep_fee_fixed = forms.DecimalField(
label=_("Keep a fixed cancellation fee"),
max_digits=10, decimal_places=2,
@@ -542,8 +570,13 @@ class EventCancelForm(forms.Form):
max_digits=10, decimal_places=2,
required=False
)
keep_fees = forms.BooleanField(
label=_("Keep payment, shipping and service fees"),
keep_fees = forms.MultipleChoiceField(
label=_("Keep fees"),
widget=forms.CheckboxSelectMultiple,
choices=[(k, v) for k, v in OrderFee.FEE_TYPES if k != OrderFee.FEE_TYPE_GIFTCARD],
help_text=_('The selected types of fees will not be refunded but instead added to the cancellation fee. Fees '
'are never refunded in when an order in an event series is only partially canceled since it '
'consists of tickets for multiple dates.'),
required=False,
)
send = forms.BooleanField(
@@ -581,6 +614,7 @@ class EventCancelForm(forms.Form):
self.fields['send_subject'] = I18nFormField(
label=_("Subject"),
required=True,
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
initial=_('Canceled: {event}'),
widget=I18nTextInput,
locales=self.event.settings.get('locales'),
@@ -589,6 +623,7 @@ class EventCancelForm(forms.Form):
label=_('Message'),
widget=I18nTextarea,
required=True,
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
locales=self.event.settings.get('locales'),
initial=LazyI18nString.from_gettext(gettext_noop(
'Hello,\n\n'
@@ -598,6 +633,7 @@ class EventCancelForm(forms.Form):
'Your {event} team'
))
)
self._set_field_placeholders('send_subject', ['event_or_subevent', 'refund_amount', 'position_or_address',
'order', 'event'])
self._set_field_placeholders('send_message', ['event_or_subevent', 'refund_amount', 'position_or_address',
@@ -607,6 +643,7 @@ class EventCancelForm(forms.Form):
required=True,
initial=_('Canceled: {event}'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
locales=self.event.settings.get('locales'),
)
self.fields['send_waitinglist_message'] = I18nFormField(
@@ -614,6 +651,7 @@ class EventCancelForm(forms.Form):
widget=I18nTextarea,
required=True,
locales=self.event.settings.get('locales'),
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
initial=LazyI18nString.from_gettext(gettext_noop(
'Hello,\n\n'
'with this email, we regret to inform you that {event} has been canceled.\n\n'
@@ -634,11 +672,10 @@ class EventCancelForm(forms.Form):
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']
change_decimal_field(self.fields['keep_fee_fixed'], self.event.currency)

View File

@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea
@@ -95,9 +95,10 @@ class OrganizerUpdateForm(OrganizerForm):
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk).exists():
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk,
event__isnull=True).exists():
raise ValidationError(
_('This domain is already in use for a different organizer.')
_('This domain is already in use for a different event or organizer.')
)
return d

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm

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