Compare commits

...

587 Commits

Author SHA1 Message Date
Raphael Michel
05e9d09e1f Don't cache files globally 2019-03-07 18:14:43 +01:00
Raphael Michel
93947cace0 Disable locking (just for a test) 2019-03-07 17:55:04 +01:00
Raphael Michel
d4eac76a8d Fix template typo 2019-02-27 09:30:43 +01:00
Raphael Michel
8889607d1c Stripe: Fix test mode recognition 2019-02-27 09:12:04 +01:00
Raphael Michel
5e9e00acec Fix tests that rely on the event wizard 2019-02-26 14:19:04 +01:00
Raphael Michel
0e89d4c0f7 Fix an AttributeError introduced in 104f84b7 2019-02-26 14:18:42 +01:00
Raphael Michel
8b3ce69425 Add clone button to event list within organizer 2019-02-26 13:10:53 +01:00
Raphael Michel
b20d1e8373 Add a second UI option to clone events 2019-02-26 13:10:53 +01:00
Raphael Michel
c278687487 Allow creating multiple events in different tabs at the same time 2019-02-26 13:10:53 +01:00
Raphael Michel
0c45e73456 Event creation: Throw user back if validation of previous step fails 2019-02-26 13:10:53 +01:00
Raphael Michel
104f84b7a8 Log change to quota when creating an item 2019-02-26 13:10:53 +01:00
Raphael Michel
ac4ecfbe69 OrderChangeManager: Fix a type error for orders without tax 2019-02-26 13:10:53 +01:00
Martin Gross
61c6cd2937 Show event date in PDF-export (Z#134372) 2019-02-23 13:47:36 +01:00
Raphael Michel
38066ca5ab Minor CSS helpers 2019-02-22 22:41:42 +01:00
Raphael Michel
373ab29701 Fix #1190 -- Voucher redemption: Default amount one if there is only one option 2019-02-22 15:41:56 +01:00
Raphael Michel
7302bba602 Add order date to CSV attendee list 2019-02-22 14:10:01 +01:00
Raphael Michel
5096121ac7 Improve QR code widget 2019-02-21 15:24:40 +01:00
Raphael Michel
ca4c21a843 Show QR code of a ticket directly from order details 2019-02-21 15:23:29 +01:00
Raphael Michel
407ecdf6c5 Fix spanish translation 2019-02-20 21:02:31 +01:00
Raphael Michel
2faeee8e9c Merge pull request #1187 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-02-20 20:54:07 +01:00
arabestia
e1bbf7139f Translated on translate.pretix.eu (Spanish)
Currently translated at 95.7% (2870 of 3000 strings)

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

powered by weblate
2019-02-20 19:53:28 +00:00
oocf
64fc38a06e Translated on translate.pretix.eu (Spanish)
Currently translated at 95.7% (2870 of 3000 strings)

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

powered by weblate
2019-02-20 19:53:27 +00:00
Alvaro Enrique Ruano
6bcf884b7a Translated on translate.pretix.eu (Spanish)
Currently translated at 95.7% (2870 of 3000 strings)

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

powered by weblate
2019-02-20 19:53:27 +00:00
Raphael Michel
d319293da8 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-02-20 19:53:26 +00:00
Raphael Michel
832c58d288 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-02-20 19:52:39 +00:00
Raphael Michel
c251e0e7d3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-02-20 17:52:59 +01:00
Raphael Michel
27437e065a Update from Weblate (#1184) 2019-02-20 17:52:26 +01:00
oocf
86534aa7cc Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-02-20 16:51:29 +00:00
oocf
379a2140c8 Translated on translate.pretix.eu (Spanish)
Currently translated at 96.0% (2843 of 2960 strings)

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

powered by weblate
2019-02-20 16:51:29 +00:00
Raphael Michel
67059fe323 Add a simple test mode (#1181)
- [x] Provide data model and configuration toggle
- [x] Allow to delete individual test orders
- [x] Add tests
- [x] Add a prominent warning message to the backend if test mode orders exist (even though test mode is off), as this leads to wrong statistics
- [x] Decide if and how to generate invoices for test orders as invoice numbers cannot be repeated or should not have gaps.
- [x] Decide if and how we expose test orders through the API, since our difference pull mechanism relies on the fact that orders cannot be deleted.
- [x] Decide if and how we want to couple test modes of payment providers?
- [ ] pretix.eu: Ignore test orders for billing
- [ ] Adjust payment providers: Mollie, bitpay, cash, fakepayment, sepadebit

![download](https://user-images.githubusercontent.com/64280/53009081-fe420d80-343a-11e9-8361-b8511c988598.png)
2019-02-20 17:51:26 +01:00
Martin Gross
8ffc96bf31 Return pdf_data localized to the order's locale (Z#131360) 2019-02-20 16:47:29 +01:00
Raphael Michel
58b688628e Disable logging of unknown hosts 2019-02-20 16:44:00 +01:00
Raphael Michel
3f7348717b Include pending_sum in mail notifications 2019-02-20 15:12:33 +01:00
Raphael Michel
90c8e0c172 Ensure attendee name in email renderer test 2019-02-20 15:09:36 +01:00
Raphael Michel
d35ad345d7 Allow to use event meta data in email templates 2019-02-20 14:33:45 +01:00
Raphael Michel
21634369a8 Fix thumbnail scaling of portrait pictures 2019-02-20 13:50:46 +01:00
Martin Gross
a2b075c0d7 Filter sensitive keys from log-messages (#1186) 2019-02-20 13:37:44 +01:00
Martin Gross
0617abe6e3 Change test eMail address to accomodate RFC2606-sensitive mailservers (Z#134234) 2019-02-20 10:03:31 +01:00
Raphael Michel
040466353c Fix order of imports 2019-02-19 15:47:47 +01:00
Raphael Michel
46b7e9467b ibanformat filter: don't fail on empty values 2019-02-19 15:01:16 +01:00
Raphael Michel
283ff3b5e5 Merge pull request #1159 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-02-19 14:16:04 +01:00
Raphael Michel
b0bb22ea38 Translated on translate.pretix.eu (Greek)
Currently translated at 0.6% (17 of 2960 strings)

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

powered by weblate
2019-02-19 13:15:41 +00:00
Alvaro Enrique Ruano
334ee98318 Translated on translate.pretix.eu (Spanish)
Currently translated at 94.4% (2794 of 2960 strings)

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

powered by weblate
2019-02-19 12:49:40 +00:00
Alvaro Enrique Ruano
c4d342029b Translated on translate.pretix.eu (Spanish)
Currently translated at 94.4% (2794 of 2960 strings)

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

powered by weblate
2019-02-19 12:49:40 +00:00
Alvaro Enrique Ruano
bc86f9c059 Translated on translate.pretix.eu (Spanish)
Currently translated at 94.4% (2794 of 2960 strings)

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

powered by weblate
2019-02-19 12:49:40 +00:00
Alvaro Enrique Ruano
51107fe4fd Translated on translate.pretix.eu (Spanish)
Currently translated at 94.2% (2787 of 2960 strings)

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

powered by weblate
2019-02-19 12:49:40 +00:00
Martin Gross
3d65c2fd51 Migration for event.plugins non-null default 2019-02-19 13:48:32 +01:00
Alexander Schwartz
9b394b3833 Enable nl2br plugin for Markdown rendering (#1162)
The frontpage text is already markdown, and will receive its formatting via the rich_text filter.
When applying the additional linebreaksbr filter, it will add unnecessary blank lines.

I'm using the hosted pretix version. 

Test for frontpage text: 

````
Test 

* test1
* test2
````

Before (screenshot): 

---

![before](https://user-images.githubusercontent.com/3957921/52533531-5428fe00-2d35-11e9-891d-f822c3524b0e.png)

----


After (screenshot): 

----
![after](https://user-images.githubusercontent.com/3957921/52533532-568b5800-2d35-11e9-9702-4c713a110c81.png)

----
2019-02-19 12:51:33 +01:00
Martin Gross
d5747084ec Fix: Make event.plugins non-Null by default 2019-02-19 12:38:32 +01:00
Raphael Michel
777772b89e Remove spaces from locale URLs 2019-02-18 15:12:45 +01:00
Raphael Michel
c202286470 Fix #212 -- Different priorization of locale sources between backend and frontend 2019-02-18 15:12:05 +01:00
Raphael Michel
0c1738b9bb Refs #212 -- Do not set user locale if switched in frontend 2019-02-18 15:05:49 +01:00
Raphael Michel
af607083cb Add a custom field renderer for checkout 2019-02-17 21:22:47 +01:00
Raphael Michel
def7918b29 Fix wrong string interpolation in invoice generation 2019-02-17 21:22:47 +01:00
Raphael Michel
0933fc848d Order search: Fight the database optimizer to actually optimize the query 2019-02-15 11:45:06 +01:00
Raphael Michel
166f8b8a2a Fix typo in require_approval webhook 2019-02-15 08:50:36 +01:00
Raphael Michel
70fcba96a5 Add __str__ methods to more models 2019-02-14 18:50:27 +01:00
Raphael Michel
2d2d62045a Do not mark orders as paid when changed to free if they require approval 2019-02-14 18:38:33 +01:00
Raphael Michel
3988f1e2f6 Fix a typo in docs 2019-02-14 18:34:41 +01:00
Raphael Michel
d3ecb92108 Remove "refunded" from state diagram 2019-02-14 18:34:37 +01:00
Raphael Michel
b3debdfb55 Order list: Add filter for canceled with and without paid fee 2019-02-14 10:15:55 +01:00
Raphael Michel
abb770a8e7 Prevent events from being set to None through the API 2019-02-14 10:15:55 +01:00
Raphael Michel
72a2d0da35 Search in e-mail adresses during checkin 2019-02-14 10:15:55 +01:00
Raphael Michel
937cec53f7 Optimize queries for pdf_data=true 2019-02-14 10:15:55 +01:00
Raphael Michel
6e4af5da64 Perform order search on database replica 2019-02-14 10:15:02 +01:00
Raphael Michel
7ed35e06ba Allow to configure a database replica 2019-02-14 10:14:23 +01:00
Raphael Michel
55841ea660 Make sure total is calculated as a Decimal 2019-02-12 16:27:37 +01:00
Raphael Michel
78544cdb30 Implement a strong locking check to avoid race conditions during payment 2019-02-12 16:24:32 +01:00
Martin Gross
37183aced7 Disable Autocomplete for Date/Time-fields 2019-02-12 16:16:12 +01:00
Raphael Michel
a7d3cb134c Fix a token mismatch 2019-02-12 15:40:06 +01:00
Raphael Michel
da8f7f163f Check-in API: Include position data 2019-02-12 15:40:06 +01:00
Raphael Michel
89d612beed Fix bug in checkinlist view 2019-02-11 16:39:50 +01:00
Raphael Michel
f23de7e2c0 Order change: Allow to ignore quotas 2019-02-11 16:15:54 +01:00
Raphael Michel
d073007fd7 Order change: Allow to keep price when changing items 2019-02-11 16:15:13 +01:00
Raphael Michel
d9d1c83218 Optimize the check-in list view 2019-02-11 15:58:39 +01:00
Raphael Michel
ae9b8bafb8 Add missing migration 2019-02-08 15:33:26 +01:00
Raphael Michel
cbf5c2ec1d Fix ZeroDivisionError if a voucher tag is given to a voucher with max_usages=0
Fix PRETIXEU-V7
2019-02-08 13:59:05 +01:00
Raphael Michel
17392f3ef4 Store subevent data for invoice lines 2019-02-08 13:56:04 +01:00
Raphael Michel
bf36ad009f Don't request a refund if there's actually no money involved 2019-02-08 11:27:09 +01:00
Martin Gross
ca9e4823e2 Fix wrong CSV-Checkinlist link 2019-02-08 10:47:17 +01:00
Raphael Michel
d505422e0f Order overview: Make table easier to read 2019-02-07 15:28:51 +01:00
Raphael Michel
33c43ce482 Add columns lines to PDF overview export 2019-02-07 15:21:17 +01:00
Raphael Michel
f273cf4960 Fix misaligned PDF report 2019-02-07 15:21:12 +01:00
Raphael Michel
afdf09eeb4 Merge branch 'master' of github.com:pretix/pretix 2019-02-07 15:02:14 +01:00
Raphael Michel
01e5872f61 Update jsonfallback again 2019-02-07 15:01:55 +01:00
Maximilian Hils
14cc31c810 Fix payment instruction display. (#1161) 2019-02-07 14:30:26 +01:00
Raphael Michel
2972129547 Sentry: Fix a bug leading to it ignoring *everything* 2019-02-06 11:16:38 +01:00
Raphael Michel
ec4227651a Do not try to reduce voucher usage below 0 2019-02-06 10:35:54 +01:00
Raphael Michel
77950de588 Voucher bulk delete: Remove cart positions as well 2019-02-06 10:28:26 +01:00
Raphael Michel
187576eee5 Fix a ProtectedError in cart handling
FIx PRETIXEU-TR
2019-02-06 10:25:53 +01:00
Raphael Michel
0e513a0985 Require specific jsonfallback version 2019-02-06 09:59:44 +01:00
Raphael Michel
1cde728ffe Order search: Add missing field to .only() call 2019-02-06 09:52:11 +01:00
Raphael Michel
76893caffc Sentry: Ignore django.security.DisallowedHost 2019-02-05 18:15:21 +01:00
Raphael Michel
a539999c04 Sentry: Do not report retried celery tasks 2019-02-05 17:19:18 +01:00
Raphael Michel
b9c570b3d8 Sentry: Tune log levels 2019-02-05 16:35:40 +01:00
Raphael Michel
48b399424a Delete voucher even if it is contained in carts
Fix PRETIXEU-R1
2019-02-05 15:47:11 +01:00
Raphael Michel
1c73f000a9 Fix TypeError
PRETIXEU-T6
2019-02-05 15:00:58 +01:00
Raphael Michel
d0721165c1 Add distinct call back in in some cases 2019-02-05 12:10:27 +01:00
Raphael Michel
bed0a0ceeb Switch from raven to sentry_sdk 2019-02-05 11:25:58 +01:00
Raphael Michel
b53ee1dc1d Bump version to 2.5.0.dev0 2019-02-04 16:06:43 +01:00
Raphael Michel
41b56c00e5 Bump version to 2.4.0 2019-02-04 16:05:32 +01:00
Raphael Michel
cb17febf7c Add squashed migration 2019-02-04 15:48:29 +01:00
Raphael Michel
07d42a4d77 Invoice exporter: Fix missing filter on query 2019-02-04 14:47:24 +01:00
Raphael Michel
e3ebf887a4 Implement Invoice.__repr__ 2019-02-04 14:07:18 +01:00
Raphael Michel
0440187e59 Do not break on empty invoices
Sentry PRETIXEU-S8
2019-02-04 14:07:01 +01:00
Raphael Michel
dfcda0fa2c Merge pull request #1158 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-02-04 13:52:53 +01:00
Maarten van den Berg
560c0a8729 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2960 of 2960 strings)

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

powered by weblate
2019-02-04 08:45:08 +00:00
Maarten van den Berg
bc80b60b04 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2960 of 2960 strings)

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

powered by weblate
2019-02-04 08:45:08 +00:00
Maarten van den Berg
08bf3648ea Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2960 of 2960 strings)

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

powered by weblate
2019-02-04 08:45:08 +00:00
Raphael Michel
f8ee7acad6 Fix import order 2019-02-04 09:38:09 +01:00
Raphael Michel
10c86869ea Sendmail: Do not fail to show logs with status "r"
Fix sentry PRETIXEU-S3
2019-02-04 09:10:19 +01:00
Raphael Michel
9034a98df9 Remove empty translation block 2019-02-01 17:38:32 +01:00
Raphael Michel
a7142fdf55 Merge pull request #1156 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-02-01 17:38:19 +01:00
Raphael Michel
ee97c46aec Translated on translate.pretix.eu (German)
Currently translated at 99.9% (2959 of 2960 strings)

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

powered by weblate
2019-02-01 16:38:02 +00:00
Raphael Michel
7063f32f24 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (2959 of 2960 strings)

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

powered by weblate
2019-02-01 16:37:37 +00:00
Raphael Michel
2ec926b7c7 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-02-01 17:27:33 +01:00
Flavia Bastos
834b5a26a5 Adjust message if there's only one addon (#1147)
Relates to #1091
2019-02-01 17:26:37 +01:00
Raphael Michel
90f08d0aca Merge pull request #1146 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-02-01 17:26:00 +01:00
Alvaro Enrique Ruano
d5c2637198 Translated on translate.pretix.eu (Spanish)
Currently translated at 95.3% (2796 of 2934 strings)

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

powered by weblate
2019-02-01 16:25:27 +00:00
Raphael Michel
f517ba51bd Added translation on translate.pretix.eu (Greek) 2019-02-01 16:25:27 +00:00
Raphael Michel
d738198ec5 Added translation on translate.pretix.eu (Greek) 2019-02-01 16:25:27 +00:00
Maarten van den Berg
b1ce58d06c Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-02-01 16:25:27 +00:00
Maarten van den Berg
b26ef74128 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-02-01 16:25:27 +00:00
Maarten van den Berg
4f8c8ea917 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-02-01 16:25:27 +00:00
Maarten van den Berg
0803b049af Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-02-01 16:25:27 +00:00
Raphael Michel
97f3fbdb80 Fix legacy field name
Sentry PRETIXEU-S0
2019-02-01 17:20:48 +01:00
Raphael Michel
434b6e4729 Offer multi-sheet order report 2019-02-01 17:20:00 +01:00
Raphael Michel
f56bceb55f Remove a redundant string 2019-02-01 16:53:37 +01:00
Raphael Michel
2aa246b3d5 Allow to exclude items from ticket generation explicitly 2019-02-01 16:48:58 +01:00
Raphael Michel
f77b551aa6 Badges: Allow to disable per-product 2019-02-01 16:48:58 +01:00
Raphael Michel
c9415cba2b Allow to add a custom text above the payment choice 2019-02-01 16:48:58 +01:00
Raphael Michel
4dae224d73 Statistics: Ellipsize long product names 2019-02-01 16:48:58 +01:00
Raphael Michel
13cc57e98b Add multi-sheet export for invoices 2019-02-01 16:45:48 +01:00
Raphael Michel
6f980b82ac Sort exporters by name alphabetically 2019-01-31 18:44:12 +01:00
Raphael Michel
f32c581a9e Add MultiSheetListExporter base class 2019-01-31 18:43:42 +01:00
Thomas Schüßler
fcadfffb92 fixed a typo (#1152) 2019-01-30 14:36:20 +01:00
Raphael Michel
9e43459879 Widget: Guard against missing xhr.responseURL in Internet Explorer 2019-01-30 12:12:25 +01:00
Raphael Michel
87424c25de Logging: Automatically serialize file objects
Sentry PRETIXEU-RY
2019-01-30 10:59:00 +01:00
Raphael Michel
acdf7d62b5 Sort migrations 2019-01-30 10:17:16 +01:00
Raphael Michel
944138f7a9 Flake8 update 2019-01-30 09:31:34 +01:00
Raphael Michel
5da2eab1fb Fix deletion order 2019-01-30 09:26:01 +01:00
Raphael Michel
d680937a6c Work around flake8 issues 2019-01-30 08:59:58 +01:00
Raphael Michel
f35c2544b6 Do not attach empty files for orders without tickets 2019-01-29 17:12:38 +01:00
Raphael Michel
0285cd12f7 Optimize SQL queries in order list and order search 2019-01-29 16:45:01 +01:00
Martin Gross
03cacace57 Fix missing/redundant favicon 2019-01-28 17:57:12 +01:00
Martin Gross
6ed016e49e Define Favicons on Organizer-level 2019-01-28 17:29:39 +01:00
Raphael Michel
da8da01614 Fix #1148 -- Reduce number of cases in which we show "Reserved" 2019-01-28 10:43:52 +01:00
Raphael Michel
9a2ea6699a Fix obsolete tests 2019-01-28 09:10:18 +01:00
Raphael Michel
51a8bac9e6 Enforce type of log data 2019-01-28 09:10:18 +01:00
Raphael Michel
303ed07504 Voucher API: Never use a list in log_action(data) 2019-01-28 09:10:18 +01:00
bastardop
c7627f631f Docs: Added Debian dependency (#1149)
the libopenjp2-7-dev Packages was required during installation on raspbian
2019-01-28 08:59:49 +01:00
Raphael Michel
604c31c6e2 Bank transfer: Allow to manually accept payments of all amounts 2019-01-28 08:49:19 +01:00
Raphael Michel
c3da6731a1 Allow to delete an event with cart positions 2019-01-25 15:59:34 +01:00
Raphael Michel
6e556ab09b Fix wrong warning message 2019-01-25 13:13:45 +01:00
Raphael Michel
16622883f6 Add referer meta tag to backend 2019-01-25 11:08:56 +01:00
Raphael Michel
cce4379d3e Type-cast cancellation fee 2019-01-24 10:11:24 +01:00
Raphael Michel
5af99f4f1a Check-in: Do not ask questions that are already answered 2019-01-23 16:05:27 +01:00
Raphael Michel
9ed49888b8 Fix #1144 -- Make invoice form all-optional in backend 2019-01-23 10:27:09 +01:00
Raphael Michel
5bfb00db73 Upgrade bs4 and be compatible to latest soupsieve 2019-01-23 09:44:03 +01:00
Raphael Michel
a031d72ca9 Widget: Follow redirects 2019-01-22 18:06:56 +01:00
Raphael Michel
15a190cdf3 Widget: Remove debug output 2019-01-22 17:23:13 +01:00
Raphael Michel
d181375479 Consistent number formatting in widget 2019-01-21 10:54:30 +01:00
Raphael Michel
d8a57b0baa Conditionally show decimal places for tax rates 2019-01-21 10:53:50 +01:00
Raphael Michel
d482bc9de0 Prevent accumulation of tax rates when copying events 2019-01-21 10:43:27 +01:00
Raphael Michel
5c030796d7 Add more information to event copy choice 2019-01-21 10:37:54 +01:00
Raphael Michel
f6eb3bfb80 Remove redundant form option 2019-01-21 10:27:44 +01:00
Raphael Michel
3703fbcacf Do not allow customers to cancel checked-in orders 2019-01-21 09:09:54 +01:00
Lukas Bockstaller
cdea6eb55e Updates the dependency versions for flake 8 (#1143) 2019-01-21 08:58:26 +01:00
Maarten van den Berg
bf1e9d47d0 Fix #1111 -- Duplicate voucher warning (#1142)
Adds a new method to Voucher that selects all distinct orders containing
a position where the Voucher has been used, and changes the Voucher
detail view to use this method for the warning.
2019-01-21 08:52:31 +01:00
Raphael Michel
350df2a3cc Merge pull request #1141 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-21 08:51:21 +01:00
Maarten van den Berg
bc6915b251 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-21 05:00:28 +00:00
Maarten van den Berg
f9c7eeff9a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-21 05:00:07 +00:00
Maarten van den Berg
247bcf0a20 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-20 06:00:09 +00:00
Maarten van den Berg
455c961fc7 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-19 21:39:08 +00:00
Maarten van den Berg
9052d4a7a9 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-19 21:13:37 +00:00
Raphael Michel
589401e8d2 Merge pull request #1140 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-18 17:38:24 +01:00
Raphael Michel
0c366a8473 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-18 16:38:00 +00:00
Raphael Michel
c9ddbd0e88 Merge pull request #1139 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-18 17:37:56 +01:00
Raphael Michel
31bf0c24f1 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-18 16:37:29 +00:00
Raphael Michel
c74386346b Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2934 of 2934 strings)

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

powered by weblate
2019-01-18 16:34:22 +00:00
Raphael Michel
725e1f019e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-01-18 17:25:27 +01:00
Raphael Michel
06eddb2c6d Self-service refund form (#1135)
* Auto-refund

* Add missing template

* Notification for requested refund

* Model-level tests

* Add front-end tests

* Default to notify
2019-01-18 17:24:42 +01:00
Raphael Michel
80b5750756 New content for / index page 2019-01-18 17:24:28 +01:00
Raphael Michel
f37d265534 Refresh design for auth and error pages 2019-01-18 17:24:28 +01:00
Raphael Michel
7c4a1e5fb8 Merge pull request #1138 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-18 16:56:39 +01:00
sohalt
9a045c76ec Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2907 of 2907 strings)

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

powered by weblate
2019-01-18 03:06:42 +00:00
Lorhan Sohaky
447b36fdd3 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 15.9% (462 of 2907 strings)

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

powered by weblate
2019-01-16 23:00:17 +00:00
Guillaume Petit
5dbd984178 Translated on translate.pretix.eu (French)
Currently translated at 80.0% (2326 of 2907 strings)

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

powered by weblate
2019-01-16 22:00:09 +00:00
Raphael Michel
95f96f8321 Fix default public name of bank transfer 2019-01-16 08:26:56 +01:00
Raphael Michel
3933032778 Merge pull request #1136 from MacLemon/tippfehler-ausbessern
Corrected language typo.
2019-01-14 22:53:09 +01:00
Pepi Zawodsky
d0b18d9f64 Corrected language typo.
Dieser Satz kein Hilfsverb.
2019-01-14 22:41:12 +01:00
Raphael Michel
71de71ed37 PDF: Fix bug with rendering name parts 2019-01-14 11:37:58 +01:00
Raphael Michel
3438d079d5 Fix a sign error 2019-01-13 11:48:52 +01:00
Raphael Michel
e7730333c2 Show refund status to customer on order page 2019-01-12 22:33:09 +01:00
Raphael Michel
e8b9f0a3ae Frontend order view: Do not recommend download for canceled orders 2019-01-12 22:19:10 +01:00
Raphael Michel
77ebd18404 Backend order view: Show canceled fees 2019-01-12 22:18:55 +01:00
Raphael Michel
2d48198c83 Ignore database-level floating point errors 2019-01-12 22:18:36 +01:00
Raphael Michel
d103b0bb84 Weblate script: pull after push 2019-01-12 17:09:18 +01:00
Raphael Michel
01411b84e4 Merge pull request #1134 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-12 17:03:02 +01:00
Raphael Michel
b7e154d8c9 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2907 of 2907 strings)

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

powered by weblate
2019-01-12 16:02:41 +00:00
Raphael Michel
f39ac96322 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2907 of 2907 strings)

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

powered by weblate
2019-01-12 16:01:53 +00:00
Raphael Michel
74db808978 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-01-12 16:55:03 +01:00
Raphael Michel
ab72b93706 Invoice: Show number of pages next to page number 2019-01-12 16:54:37 +01:00
Raphael Michel
af5aece639 Add beneficiaries to invoice addresses 2019-01-12 16:54:37 +01:00
Raphael Michel
228ab15900 Bank transfer: Allow to set custom public name 2019-01-12 16:54:37 +01:00
Raphael Michel
66164d8202 Invoice renderer: Make it easier to change fonts 2019-01-12 16:54:37 +01:00
Raphael Michel
d5ac155914 Add is_available hook for plugin configs 2019-01-12 16:54:37 +01:00
Raphael Michel
75a966529e Merge pull request #1132 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-11 15:57:47 +01:00
Raphael Michel
28a6a6185d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2904 of 2904 strings)

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

powered by weblate
2019-01-11 14:57:27 +00:00
Raphael Michel
07cdaa9ca9 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2904 of 2904 strings)

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

powered by weblate
2019-01-11 14:57:13 +00:00
Raphael Michel
1c6935ebd9 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-01-11 15:47:30 +01:00
Raphael Michel
60c1ea8aad Allow to keep cancellation fees (#1130)
* Allow to keep cancellation fees

* Add tests and clarifications

* Add API
2019-01-11 15:42:33 +01:00
Raphael Michel
0b8798a65c Add self-crashing test 2019-01-10 18:17:29 +01:00
Raphael Michel
a8836cbeec Remove some irregularities in 8abfbba9 2019-01-10 17:41:18 +01:00
Raphael Michel
336a34b10b Merge pull request #1131 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-10 17:24:31 +01:00
Raphael Michel
c5862cc0a0 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2901 of 2901 strings)

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

powered by weblate
2019-01-10 16:23:32 +00:00
Raphael Michel
89cdcd3781 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2901 of 2901 strings)

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

powered by weblate
2019-01-10 16:21:10 +00:00
Raphael Michel
2837cac554 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-01-10 16:57:52 +01:00
Raphael Michel
3b54556739 Remove notification type for refunded event 2019-01-10 16:57:27 +01:00
Raphael Michel
4d6d6ff737 Merge pull request #1129 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-10 16:57:10 +01:00
Maarten van den Berg
ffee31e415 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-10 15:56:44 +00:00
Raphael Michel
8abfbba9d0 Refactor cancelling positions and orders in the data model (#1088)
- [x] Data model
- [x] display in order view in backend
- [x] review all usages of OrderPositions.objects
- [x] review all usages of order.positions
- [x] review all other model usages
- [x] review plugins
- [x] plugins backwards-compatible API?
- [x] decide on way forward for REST API
- [x] need to cancel fees
- [x] tests
- [ ] plugins
  - [ ] gdpr
  - [ ] reports
- [x] docs
2019-01-10 16:52:34 +01:00
Raphael Michel
588955901c Pin oauthlib version 2019-01-09 14:19:02 +01:00
Raphael Michel
4b7bf2f27f Merge pull request #1118 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2019-01-09 12:33:18 +01:00
Raphael Michel
664957e886 Add now_date 2019-01-09 12:29:37 +01:00
Raphael Michel
f15a6d39c3 Add now_* variables to PDFs 2019-01-09 12:17:31 +01:00
Maarten van den Berg
3fd80a9a46 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-08 21:00:37 +00:00
Maarten van den Berg
2fd2716303 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-08 21:00:08 +00:00
Maarten van den Berg
37315fc380 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-08 13:00:29 +00:00
Maarten van den Berg
f96fc0744e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-08 12:30:26 +00:00
Maarten van den Berg
5bb7883020 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-08 12:25:27 +00:00
Maarten van den Berg
3f95434922 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
08da5a8b91 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
97dc4421ea Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
26ca2ff006 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
980c359f57 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 2.9% (2 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
ff1198dec6 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Raphael Michel
7275de94af Added translation on translate.pretix.eu (Dutch (informal)) 2019-01-07 09:21:52 +00:00
Raphael Michel
ed46f41f8c Added translation on translate.pretix.eu (Dutch (informal)) 2019-01-07 09:21:52 +00:00
Alexey Zh
1078e38890 Translated on translate.pretix.eu (Russian)
Currently translated at 16.2% (11 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
amefad
2e9bbff308 Translated on translate.pretix.eu (Italian)
Currently translated at 33.8% (23 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
13a48701fa Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Alexey Zh
ddc9c850c0 Translated on translate.pretix.eu (Russian)
Currently translated at 0.4% (13 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
amefad
fa0dae6ed6 Translated on translate.pretix.eu (Italian)
Currently translated at 2.7% (79 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
da6176a51e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Maarten van den Berg
4ef6659551 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Alexey Zh
82624a1dc0 Added translation on translate.pretix.eu (Russian) 2019-01-07 09:21:52 +00:00
Alexey Zh
b50add260a Added translation on translate.pretix.eu (Russian) 2019-01-07 09:21:52 +00:00
Bruno Damasceno Martins
f72f97d366 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 10.4% (300 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Alexander Schwartz
ad46e9e541 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2019-01-07 09:21:52 +00:00
Raphael Michel
343dbc00be Bank transfer: Add a note on how to proceed 2019-01-07 10:21:35 +01:00
Raphael Michel
3cb94f702d Revert accidental commit part 2019-01-04 10:24:06 +01:00
Raphael Michel
ddeae224fb Log SMTP failures and retry after some error codes 2019-01-04 09:54:43 +01:00
Raphael Michel
3c57895101 Don't mark orders as pending unnecessarily 2019-01-03 09:50:56 +01:00
Raphael Michel
687c85eb58 Stripe: Full source state handling 2019-01-03 09:43:07 +01:00
Raphael Michel
90ffdbdfa3 Stripe: Allow failed payments to succeed 2019-01-03 09:37:31 +01:00
Raphael Michel
654be0db34 Stripe: Prevent race condition between ReturnView and webhook 2019-01-03 09:34:15 +01:00
Raphael Michel
82e3359b40 Allow to filter subevents by date 2019-01-02 16:59:03 +01:00
Raphael Michel
01a6861453 Always query emails case-insensitively 2019-01-02 15:12:48 +01:00
Raphael Michel
7f6cdd6241 Fix ProtectedError when deleting expired card positions 2019-01-02 15:05:30 +01:00
Raphael Michel
aad1fda31f Register nl_Informal translation 2019-01-02 10:12:17 +01:00
Raphael Michel
ad462921f0 Pin bs4 version due to regression 2019-01-02 09:58:50 +01:00
Raphael Michel
dc433f6420 Reverse on global URL config in build_absolute_uri 2019-01-02 09:24:47 +01:00
Raphael Michel
2d8b3d1c79 PayPal: Fix backwards compatibility 2019-01-02 09:20:35 +01:00
Raphael Michel
eb85fa956e Badges: Add per-position downloads 2018-12-19 12:31:44 +01:00
Raphael Michel
215514fca7 Add ticket downloads to the backend 2018-12-19 12:31:24 +01:00
Raphael Michel
3fe2dfe810 Add signal order_position_buttons 2018-12-19 12:29:52 +01:00
Raphael Michel
041d05eb66 Support product pictures for add-on products 2018-12-19 09:37:30 +01:00
Raphael Michel
d05530ddfc Explicit ordering of check-in lists 2018-12-19 09:20:44 +01:00
Raphael Michel
734e77d1a3 API: Allow to redeem ticket by secret 2018-12-18 12:23:07 +01:00
Raphael Michel
633061e203 Avoid paid orders without payment_date 2018-12-18 10:07:17 +01:00
Raphael Michel
e11ee4a427 Do not allow to delete vouchers assigned to canceled orders 2018-12-18 10:07:17 +01:00
Alvaro Enrique Ruano
1edcd47703 Support for daterange in spanish (#1125) 2018-12-17 22:32:15 +01:00
Raphael Michel
cf4b2544f2 Never create implicit payments for orders that require approval 2018-12-14 10:42:08 +01:00
Raphael Michel
04c3cffd43 Fix severe translation mistake 2018-12-12 16:42:47 +01:00
Raphael Michel
483d41c7a6 Event plugin list: Use a more useful sorting of the list 2018-12-12 16:42:47 +01:00
Martin Gross
b0c4c88d01 Fix #1119 - Proper indent and pluralisation (#1120) 2018-12-12 08:59:54 +01:00
Martin Gross
518298f71c Add media-src CSP to middleware (#1121) 2018-12-12 08:59:22 +01:00
Raphael Michel
62c2e7765b Fix wrong variable 2018-12-11 17:00:05 +01:00
Raphael Michel
2bb2a40509 Add new signal checkout_all_optional 2018-12-11 16:44:15 +01:00
Raphael Michel
49828186b0 Signals: Pretictable call order, not return order 2018-12-11 16:43:07 +01:00
Raphael Michel
c07a6cb4aa Small query optimization 2018-12-11 16:15:54 +01:00
Tobias Kunze
67ad9a0dcb Provide send_robust on EventSignals (#1116) 2018-12-11 16:15:22 +01:00
Raphael Michel
d267dfc682 Fix #785 -- Show availability in (sub)event list (#1112) 2018-12-11 13:59:49 +01:00
Raphael Michel
eed220f14a pytest-xdist is required by now 2018-12-11 12:54:30 +01:00
Raphael Michel
85289fe0d1 Fix error when marking an expired order as paid 2018-12-07 11:04:41 +01:00
Raphael Michel
6293ad34d4 Add a headline to payment provider settings 2018-12-06 10:56:40 +01:00
Raphael Michel
0dc4f61cf0 Fix a docs spelling error 2018-12-06 10:42:50 +01:00
Raphael Michel
6849e682d7 Bump verstion to 2.4.0.dev0 2018-12-06 10:17:39 +01:00
Raphael Michel
6b725a9db9 Bump version to 2.3.0 2018-12-06 10:16:55 +01:00
Raphael Michel
989ebbb444 Merge pull request #1115 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-12-05 17:13:35 +01:00
Raphael Michel
0a6efc1e0f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2018-12-05 16:12:43 +00:00
Raphael Michel
d577a0d286 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2890 of 2890 strings)

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

powered by weblate
2018-12-05 16:11:04 +00:00
Raphael Michel
6b9b379ce2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-12-05 17:05:14 +01:00
Raphael Michel
13234b6fd5 Merge pull request #1108 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-12-05 17:04:36 +01:00
Raphael Michel
2fa0067663 Revert "Update po files"
This reverts commit 4e37fa5778.
2018-12-05 17:04:22 +01:00
Raphael Michel
4e37fa5778 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-12-05 17:02:53 +01:00
Alexander Schwartz
bfb74448b1 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (69 of 69 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
a255082b07 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2797 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Matheus Nunes
14df35bd90 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 10.4% (300 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
bd0ba7baa5 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
9aa220b95b Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
3ed4be63fe Translated on translate.pretix.eu (German (informal))
Currently translated at 100,0% (69 of 69 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Alexander Schwartz
23f4b0b62f Translated on translate.pretix.eu (German)
Currently translated at 100,0% (69 of 69 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
4b9acb64da Translated on translate.pretix.eu (Spanish)
Currently translated at 98.6% (68 of 69 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
oocf
ebba0ee0cb Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2796 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
arabestia
335ce48d7e Translated on translate.pretix.eu (Spanish)
Currently translated at 97.3% (2796 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Alvaro Enrique Ruano
d9a0c8c523 Translated on translate.pretix.eu (Spanish)
Currently translated at 96.7% (2779 of 2874 strings)

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

powered by weblate
2018-12-05 15:59:59 +00:00
Raphael Michel
a297bd1944 Show that some export parameters are optional 2018-12-05 16:51:08 +01:00
Raphael Michel
953ea26984 Add a custom text to explain usage of vouchers 2018-12-05 16:50:05 +01:00
Raphael Michel
e4f80f7660 Widget: Allow to pre-fill fields in the invoice address 2018-12-05 16:45:05 +01:00
Raphael Michel
128a185957 Improve crashed test handling 2018-12-05 16:16:50 +01:00
Raphael Michel
0bdd14b47a Re-add legacy import 2018-12-05 15:13:50 +01:00
Raphael Michel
3b84b181ad PDF editor: Move questions signal out of ticket provider 2018-12-05 14:45:07 +01:00
Raphael Michel
c9b0626324 Adjust tests to new is_public default 2018-12-04 15:41:57 +01:00
Raphael Michel
dc9a82cade Fix failing tests for previous commits 2018-12-04 15:02:16 +01:00
Raphael Michel
8266733e34 Clarify is_public and turn it on by default 2018-12-04 13:49:25 +01:00
Raphael Michel
246987955b Accept localized input for all fields with localized output 2018-12-02 19:12:21 +01:00
Raphael Michel
b93e7fcb60 Fix #1067 -- Allow to manually create partial payments 2018-12-02 18:32:16 +01:00
Raphael Michel
b1cebdbd99 Fix #582 -- Improve validation of organizer domains 2018-12-02 17:45:48 +01:00
Raphael Michel
d04047abd5 Fix #1105 -- Provide URL in order split log entry 2018-12-02 17:35:31 +01:00
Raphael Michel
efca46945a Fix #953 -- Render markdown in email text previews 2018-12-02 17:11:09 +01:00
Raphael Michel
0f9755e36f Add a warning message to products that are out of timeframe 2018-12-02 16:44:21 +01:00
Raphael Michel
478d8e4116 Add export to .xlsx for lists 2018-11-30 16:10:32 +01:00
Raphael Michel
81693e042c Introduce common base class for CSV exports 2018-11-30 15:56:29 +01:00
Raphael Michel
47b7d7b36c Add separate notification category for orders that require approval 2018-11-30 15:30:35 +01:00
Raphael Michel
ba15c34ce1 Fix #1106 -- Do not send reminders to orders placed in the last two hours 2018-11-30 15:16:12 +01:00
Raphael Michel
94f2ad9325 Highlight items that are unavailable by time 2018-11-29 10:13:43 +01:00
Raphael Michel
d8070ba8a3 Fix missing ticket attachments 2018-11-29 10:01:20 +01:00
Raphael Michel
b1019672b0 Fix file format in real expor 2018-11-27 15:52:57 +01:00
Raphael Michel
631307a4d5 Even with pdftk, use PyPDF to read page size 2018-11-27 09:22:53 +01:00
Raphael Michel
180a26ee1d Fix shredder test 2018-11-26 13:44:40 +01:00
Raphael Michel
7eab1982fe Add support for PDFTK 2018-11-26 13:43:06 +01:00
Raphael Michel
ca59237ebf Use regular asynctasks for order PDF generation 2018-11-26 13:21:25 +01:00
Raphael Michel
cc92210dc2 Retry crashed tests 2018-11-26 12:09:39 +01:00
Raphael Michel
6602afdd6c Use dedicated queue for notifications 2018-11-26 12:05:16 +01:00
Raphael Michel
c7a04bc08a Add cleanup for cached tickets 2018-11-26 12:04:25 +01:00
Raphael Michel
2cc5b7f4e8 Raise error 404 on invalid month 2018-11-26 09:20:48 +01:00
Raphael Michel
453f16af03 Merge pull request #1100 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-23 16:22:00 +01:00
Raphael Michel
0f3398ae13 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
f1b65c8695 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
2c4c89c8c2 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
4042b603b7 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2874 of 2874 strings)

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

powered by weblate
2018-11-23 15:17:00 +00:00
Alvaro Enrique Ruano
63b0288383 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.5% (2792 of 2864 strings)

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

powered by weblate
2018-11-23 15:17:00 +00:00
Raphael Michel
7c01fee70b Fix incorrect return statement 2018-11-23 16:16:46 +01:00
Raphael Michel
8127c32ef5 More tolerant i18n deserializing 2018-11-23 16:15:23 +01:00
Raphael Michel
563decdfba Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-23 15:37:11 +01:00
Raphael Michel
a205b01d70 Add "paid" note on invoices if there is no open payment 2018-11-23 15:36:39 +01:00
Raphael Michel
b4290384e1 Add sales channels (#1103)
- [x] Data model
- [x] Enforce constraint
- [x] Filter order list
- [x] Set channel on created order
- [x] Products API
- [x] Order API
- [x] Tests
- [x] Filter reports
- [x] Resellers
- [ ] deploy plugins
  - [ ] posbackend
  - [ ] resellers
  - [ ] reports
- [x] Ticketlayouts
- [x] Support in pretixPOS
2018-11-23 15:35:09 +01:00
Raphael Michel
0f76779fb1 Fix involuntarily created invoices 2018-11-21 13:07:28 +01:00
Raphael Michel
f34c528cba Add passphrase to wordlist 2018-11-21 11:51:57 +01:00
Raphael Michel
cf01e04101 PayPal: Improve log display 2018-11-21 11:24:44 +01:00
Martin Gross
a3a63def55 Fix #369 -- Connect with PayPal (#1084)
* Connect with PayPal

* PayPal connect code-review fixes

* PayPal Connect: Global Env selection; Fix for payee-dict

* Fix missing PayPal Connect indicator for Endpoint

* Fix backwards compatibility
2018-11-21 11:14:33 +01:00
Raphael Michel
a3489eea04 PayPal: Properly detect pending sales 2018-11-21 11:14:03 +01:00
Raphael Michel
c6cb98c30a Add documentation on development and enterprise installs 2018-11-21 09:40:44 +01:00
Alvaro Enrique Ruano
332c58c82f Improve docker cache utilization on image construction (#1099)
To provide better use of Docker cache during image build by installing from PIP before creating the layer with the source code (the PIP installations are less probable to change than the rest of the source code, so this layer will not be recreated with all the source code changes)

This was previously discussed in #1094
2018-11-21 09:27:43 +01:00
Raphael Michel
beb0ded6dc Allow to pass user data to the widget (#1095)
- [x] Logic
- [x] Tests
- [x] Docs
- [x] find a way to integrate with tracking
2018-11-20 17:55:37 +01:00
Raphael Michel
b49b2035bd Cancel payments if the pending price of the order changes 2018-11-20 17:41:33 +01:00
Raphael Michel
106c8d373d Fix #1098 -- order search with dash in event slug 2018-11-20 14:33:41 +01:00
Raphael Michel
aee44a3284 Fix marking an overpaid order as paid manually 2018-11-20 10:39:48 +01:00
Raphael Michel
d4c1fcf838 Merge pull request #1087 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-20 10:39:26 +01:00
Alvaro Enrique Ruano
832f57c9d7 Translated on translate.pretix.eu (Spanish)
Currently translated at 97.4% (2790 of 2864 strings)

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

powered by weblate
2018-11-20 09:36:17 +00:00
Mikkel Ricky
ac2a9b207d Translated on translate.pretix.eu (Danish)
Currently translated at 57.4% (1645 of 2864 strings)

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

powered by weblate
2018-11-20 09:36:17 +00:00
Maarten van den Berg
f1e5d60a14 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2864 of 2864 strings)

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

powered by weblate
2018-11-20 09:36:17 +00:00
Maarten van den Berg
7b1a1dc754 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.7% (2854 of 2864 strings)

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

powered by weblate
2018-11-20 09:36:17 +00:00
Raphael Michel
c93f804992 Fix #1080 -- Deal with gaps in the invoice database (#1086) 2018-11-20 10:36:13 +01:00
Alvaro Enrique Ruano
1cba4b1d45 Dockerfile improvements for better readability (#1094) 2018-11-20 10:24:53 +01:00
Raphael Michel
22369a5559 Fix CI problems with SQLite (#1076)
* Try to re-enable tests on Py3.5

* Add newer SQLite3
2018-11-19 17:57:50 +01:00
Raphael Michel
a8223ad354 Fix missing return statement 2018-11-19 17:12:48 +01:00
Raphael Michel
c9d3cf7cac Fix exceptions in previous commit 2018-11-19 11:03:54 +01:00
Raphael Michel
bbdbc94f6e Redirect case-insensitive versions of event/organizer slugs 2018-11-19 10:22:40 +01:00
Raphael Michel
5c8d9c4dca Fix incorrect feedback on invite form 2018-11-16 14:13:44 +01:00
Raphael Michel
546ff6e42f Variations: Show a price range on the front page 2018-11-14 15:43:21 +01:00
Raphael Michel
7b7d45ce2e Fix Dutch date format 2018-11-14 15:43:15 +01:00
Raphael Michel
be3ca7c561 Sort cart positions by reasonable values 2018-11-14 15:36:50 +01:00
Raphael Michel
abdb6e2d52 Add "Event date" to PDF editors 2018-11-14 14:39:20 +01:00
Raphael Michel
138ddcdcd7 CSV-export improvements (include voucher, allow semicolon) 2018-11-14 10:02:28 +01:00
Raphael Michel
8ffc6550da Do not allow orders with unavailable items to be completed 2018-11-13 17:55:56 +01:00
Raphael Michel
0734715bab Only warn about bad-contrasat colors 2018-11-13 15:53:18 +01:00
Raphael Michel
7528bfb10b Fix tests for color saving 2018-11-13 14:15:52 +01:00
Raphael Michel
2798fb3468 Avoid setting name_parts to None 2018-11-13 12:59:12 +01:00
Raphael Michel
4e6f4716ec Allow to configure accent colors 2018-11-13 12:56:52 +01:00
Raphael Michel
e523a4e610 Allow to manually generate invoices like in c131ad8c 2018-11-12 13:08:12 +01:00
Raphael Michel
31cec76809 Generate invoice after expired order is extended 2018-11-12 13:08:12 +01:00
Raphael Michel
fdfd9f9275 Fix cart cleanup 2018-11-12 13:08:12 +01:00
oocf
b658c73c19 Removed permissions page from events settings (#1063)
* Removed old configuration for events settings

* Urls file to remove permissions file

* Removing not needed test

* Removing test in permissions
2018-11-12 12:23:40 +01:00
Raphael Michel
ebd3e6f31a Fix TypeError in typeahead 2018-11-12 12:17:49 +01:00
Raphael Michel
ccec114653 Merge pull request #1085 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-12 11:41:06 +01:00
Raphael Michel
f0716dc482 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2864 of 2864 strings)

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

powered by weblate
2018-11-12 10:40:47 +00:00
Raphael Michel
513778b2c4 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2864 of 2864 strings)

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

powered by weblate
2018-11-12 10:34:26 +00:00
Raphael Michel
742e403ae2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-12 11:31:32 +01:00
Raphael Michel
09a9d610f8 Make navigation structure more approachable to new users (#1083)
* Move event selector to sidebar

* Unify navigation

* Fix confusing icons
2018-11-12 11:30:36 +01:00
Raphael Michel
b9534f23f5 Delete add-ons explicitly 2018-11-12 11:11:33 +01:00
Raphael Michel
b053f61001 Delete add-on positions explicitly 2018-11-12 11:11:33 +01:00
Felix Rindt
21042f2111 Fix #1071 -- Make payments and invoice address full width panels (#1072)
Solve #1071.

I'm not happy about how the invoice address panel is really wide now.
2018-11-12 10:38:22 +01:00
Raphael Michel
e953474138 Fix a few models.CASCADE 2018-11-11 16:23:37 +01:00
Tobias Kunze
0d438ad07c Remove outline when clicking on checkout progress (#1082) 2018-11-10 14:48:37 +01:00
Raphael Michel
e285b7cff0 Bump version to release 2.3.0.dev0 2018-11-09 16:45:43 +01:00
Raphael Michel
2bb2f30e66 Bump version to 2.2.0 2018-11-09 16:43:59 +01:00
Raphael Michel
9a8d23f582 Banktransfer: use proper formatting for IBANs 2018-11-09 16:42:43 +01:00
Raphael Michel
f37d12e056 Merge pull request #1079 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-09 16:26:35 +01:00
Maarten van den Berg
334ffc0be7 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2855 of 2855 strings)

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

powered by weblate
2018-11-09 14:18:13 +00:00
Maarten van den Berg
03f0da4ee6 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (66 of 66 strings)

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

powered by weblate
2018-11-09 14:18:13 +00:00
Raphael Michel
fbbd6eebc0 Refuse to update on old MySQL 2018-11-09 15:17:58 +01:00
Raphael Michel
584ced87db Add /me API endpoint 2018-11-09 11:34:40 +01:00
Raphael Michel
901953d988 Add a Retry-After to 409 responses 2018-11-09 11:13:24 +01:00
Raphael Michel
8c34a47138 Voucher API: Bulk creation 2018-11-09 10:50:21 +01:00
Raphael Michel
0fe3db634c Voucher API: Reduce number of event locks 2018-11-09 10:46:27 +01:00
Raphael Michel
d8d838fc4f Add note on celery-requirement for webhook retrials 2018-11-09 10:45:25 +01:00
Raphael Michel
9b94a1b3b2 Add documentation on rate limits 2018-11-09 10:00:55 +01:00
Raphael Michel
479abc1a65 Add missing screenshots to docs 2018-11-08 17:00:18 +01:00
Raphael Michel
1a17ba13ca Link to documentation 2018-11-08 16:57:30 +01:00
Raphael Michel
371c42b738 Merge pull request #1077 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-08 16:57:18 +01:00
Raphael Michel
ed85394845 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2855 of 2855 strings)

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

powered by weblate
2018-11-08 15:56:22 +00:00
Raphael Michel
a9a684a456 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (66 of 66 strings)

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

powered by weblate
2018-11-08 15:55:48 +00:00
Raphael Michel
d7d7792a4a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (66 of 66 strings)

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

powered by weblate
2018-11-08 15:55:48 +00:00
Raphael Michel
c09587f5d3 Translated on translate.pretix.eu (German)
Currently translated at 99.8% (2848 of 2855 strings)

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

powered by weblate
2018-11-08 15:55:48 +00:00
Raphael Michel
23f719381c Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2855 of 2855 strings)

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

powered by weblate
2018-11-08 15:55:48 +00:00
Raphael Michel
d74d39d6e9 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (66 of 66 strings)

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

powered by weblate
2018-11-08 15:55:48 +00:00
Raphael Michel
5f2cf8d3ef Add documentation on webhooks 2018-11-08 16:53:25 +01:00
Raphael Michel
1843799345 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-08 16:40:19 +01:00
Raphael Michel
bd838b3b7c Fix #1074 -- More specific messages during asynctasks 2018-11-08 16:38:12 +01:00
Raphael Michel
c2d03f5e6b Fix #526 -- Add a webhook system (#1073)
- [x] Data model
- [x] UI
- [x] Fire hooks
- [x] Unit tests
- [x] Display logs
- [x] API to modify hooks
- [x] Documentation
- [x] More hooks!
2018-11-08 16:38:05 +01:00
Raphael Michel
74e8e73877 Stop testing against Py3.5 2018-11-08 15:44:56 +01:00
Raphael Michel
8830dc8f78 Fix tests for checkin list 2018-11-08 12:04:51 +01:00
Raphael Michel
ac877a7c0d Use 3 SQLite workers 2018-11-08 11:34:02 +01:00
Raphael Michel
0a442e712b Add company to checkin list 2018-11-08 11:01:29 +01:00
Raphael Michel
4477f8001e Adjust test for previous commit 2018-11-07 11:53:13 +01:00
Raphael Michel
152b94428f Make Item.allow_delete() more consistent 2018-11-07 11:19:23 +01:00
Raphael Michel
5390b0b191 API: Allow to sort orders by modification date 2018-11-07 10:29:41 +01:00
Raphael Michel
97de8cea08 Allow cart creation without attendee name 2018-11-06 18:16:54 +01:00
Raphael Michel
cd465c1aad Fix not passing an attendee name in the API 2018-11-06 17:42:18 +01:00
Raphael Michel
449dea41a8 Fix order list export 2018-11-06 14:57:35 +01:00
Raphael Michel
0b1a6e4745 Fix symmetry 2018-11-06 14:57:22 +01:00
Tobias Kunze
e49061e28c Don't check voucher quotas if they bypass quotas (#1070) 2018-11-06 11:01:05 +01:00
Raphael Michel
18cb29b425 Show date in event picker 2018-11-05 22:45:33 +01:00
Raphael Michel
994ff23719 Fix quick event switcher on mobile 2018-11-05 22:24:26 +01:00
Raphael Michel
15d077df6e Add explanation tooltips to invoice regeneration buttons 2018-11-05 21:46:16 +01:00
Raphael Michel
b490aa7f5d Add scheme to sample names 2018-11-05 21:35:44 +01:00
Raphael Michel
ca6b3badde Fix reference to removed field 2018-11-05 21:20:17 +01:00
Raphael Michel
1f200271af Allow rich text in question help texts 2018-11-05 18:07:15 +01:00
Raphael Michel
894a60d016 Merge pull request #1069 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-05 16:56:15 +01:00
Raphael Michel
4a2219134b Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2825 of 2825 strings)

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

powered by weblate
2018-11-05 15:55:43 +00:00
Raphael Michel
7d38fc5c03 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2825 of 2825 strings)

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

powered by weblate
2018-11-05 15:50:02 +00:00
Raphael Michel
ef5de187b9 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2825 of 2825 strings)

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

powered by weblate
2018-11-05 15:46:16 +00:00
Raphael Michel
a1c424266b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-11-05 16:36:29 +01:00
Raphael Michel
557b4b7b6f Merge pull request #1060 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-11-05 16:36:02 +01:00
oocf
98be21253d Translated on translate.pretix.eu (Spanish)
Currently translated at 98.4% (2750 of 2794 strings)

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

powered by weblate
2018-11-05 15:24:40 +00:00
Maarten van den Berg
e5a04ada94 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2794 of 2794 strings)

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

powered by weblate
2018-11-05 15:24:40 +00:00
Maarten van den Berg
9b8b3090e6 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2794 of 2794 strings)

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

powered by weblate
2018-11-05 15:24:40 +00:00
Raphael Michel
e622c3948d Fix buggy migration 2018-11-05 16:24:30 +01:00
Raphael Michel
94be46ffdb Fix #978 -- Allow to split names (#1049)
- [x] attendee names
- [x] Invoice address names
- [x] Data migration
- [x] API serializers
  - [x] orderposition
  - [x] cartposition
  - [x] invoiceaddress
  - [x] checkinlistposition
- [x] position API search
- [x] invoice API search
- [x] business/individual required toggle
- [x] Split columns in CSV exports
- [x] ticket editor
- [x] shredder
- [x] ticket/invoice sample data
- [x] order search
- [x] Handle changed naming scheme
- [x] tests
- [x] make use in:
  - [x] Boabee
  - [x] Certificate download order
  - [x] Badge download order
  - [x] Ticket download order
- [x] Document new MySQL requirement
- [x] Plugins
2018-11-05 15:43:21 +01:00
Raphael Michel
7039374588 Allow to anonymize users 2018-11-05 11:11:43 +01:00
Raphael Michel
0a5347c08b Allow to delete organizers 2018-11-05 11:11:43 +01:00
Raphael Michel
87f3318431 Merge pull request #1006 from pretix/empty_vouchers
Allow to show all vouchers with empty tags
2018-11-05 10:35:22 +01:00
Tobias Kunze
2557a8e4ec Allow to show all vouchers with empty tags 2018-11-05 10:34:25 +01:00
Raphael Michel
aff7094cb0 Fix #1021 -- Bugs in SMTP test 2018-11-05 10:19:29 +01:00
Raphael Michel
5a29b4bf70 Allow to choose French and Spanish 2018-10-31 15:38:38 +01:00
Raphael Michel
e618183b49 Merge pull request #1059 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-10-31 15:38:01 +01:00
Raphael Michel
a18236b12d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-31 14:34:07 +00:00
Raphael Michel
b5da4e89a6 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2794 of 2794 strings)

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

powered by weblate
2018-10-31 14:33:21 +00:00
Raphael Michel
1da2737427 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2794 of 2794 strings)

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

powered by weblate
2018-10-31 14:30:18 +00:00
Raphael Michel
032fdadc3c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-31 14:26:13 +00:00
Raphael Michel
8ae3ff3fe6 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-10-31 15:21:52 +01:00
Raphael Michel
b8669503fa Only allow restricting payment countries if invoice address is obligatory 2018-10-31 15:21:26 +01:00
Raphael Michel
863165caaa Gracefully handle PayPal exceptions 2018-10-31 15:21:26 +01:00
Raphael Michel
b885f30789 Update from Weblate. (#1039) 2018-10-31 15:18:40 +01:00
Mattias de Hollander
461b62bd51 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Maarten van den Berg
23776db3b6 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Arnaud Vergnet
19e91a6c7c Translated on translate.pretix.eu (French)
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Samir C. Costa
6f40325d3f Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Samir C. Costa
1987bff4b1 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 100,0% (65 of 65 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Maarten van den Berg
5aa0d55d47 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.0% (2746 of 2773 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Raphael Michel
a28196e930 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Raphael Michel
c55387819d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-31 09:43:05 +00:00
Raphael Michel
c8cc527aee OrderChangeManager: Do not mark order pending when adjusting price to actual payment 2018-10-31 10:42:44 +01:00
Raphael Michel
a39b207ad5 Mark manual payment failed if nothing happened 2018-10-31 10:33:23 +01:00
Raphael Michel
ea63b50f2e Bank transfer: consider payments valid even without quota 2018-10-31 10:31:40 +01:00
Raphael Michel
b101251aa4 Log confirmed payments that could not mark the order as paid 2018-10-31 10:31:21 +01:00
Raphael Michel
c9ba72ebc5 Fix a typo 2018-10-31 10:27:14 +01:00
Raphael Michel
4a1c3088a9 Locking in OrderChangeManager caused unexpected problems 2018-10-31 10:04:53 +01:00
Raphael Michel
a480ca1142 Add reverse charge flag to invoices 2018-10-30 10:57:29 +01:00
Tobias Kunze
a928fbfafe Config files (#1057)
* Move coveragerc to setup.cfg

* Move pytest.ini to setup.cfg

Closes #1027
2018-10-30 10:12:17 +01:00
Tobias Kunze
3bf3ff1ee2 Allow empty plugin responses (#1056)
While plugin developers are supposed to return an empty dictionary, it's
conceivable that they might just put in a `return` if their field is not
needed, and pretix being generous about this would be cool.
2018-10-30 10:11:39 +01:00
Raphael Michel
9647cc6cf2 Add more favicons for browser shortcuts 2018-10-30 10:05:48 +01:00
Raphael Michel
df2d8925ed Prevent some race conditions 2018-10-29 17:27:12 +01:00
Raphael Michel
7a945daefc Fix #957 -- Integrate BezahlCode and GiroCode 2018-10-29 12:57:26 +01:00
Raphael Michel
409e77cf2f Stop pinning setuptools 2018-10-29 12:42:07 +01:00
Raphael Michel
552f99a63b Read PDF backgrounds with strict=False 2018-10-29 11:41:26 +01:00
Raphael Michel
0842311451 PDF renderer: Do not break on wrong poweredby-styles 2018-10-29 11:41:26 +01:00
Raphael Michel
4d4b498636 Resolve bug in event copy signals of pdf output and badges 2018-10-29 11:41:26 +01:00
Raphael Michel
d08cc12240 Do not break on short VAT IDs 2018-10-29 11:41:26 +01:00
Raphael Michel
237442872e Reliably delete addons when deleting cart positions 2018-10-29 11:41:25 +01:00
Raphael Michel
16983826fb Allow to store structured invoice addresses 2018-10-24 01:37:18 +02:00
Raphael Michel
e60ff6b777 Allow to store strucutred SEPA bank transfer details 2018-10-24 00:21:33 +02:00
Raphael Michel
3a0ef3760c Make logging for payment changes consistent with OrderPayment handling 2018-10-22 22:28:42 +02:00
Raphael Michel
bc0bc78219 Try to fix invoice export 2018-10-22 06:23:09 +02:00
Tobias Kunze
d3137505a1 Don't offer to check empty VAT ID (#1044) 2018-10-17 01:50:24 +02:00
Flavia Bastos
a2acd336eb Fix #970 -- invoice info consistency (#1043)
resolves: Issue #970
2018-10-17 01:50:00 +02:00
Raphael Michel
6e4750336b Fix test case for previous commit 2018-10-12 11:44:46 +02:00
Raphael Michel
ddefeeaf02 Waiting list should send things out even if waiting list is disabled 2018-10-12 10:45:33 +02:00
Raphael Michel
250e0a930d Prevent huge invoice files if a JPEG logo is used 2018-10-09 10:56:11 +02:00
Tobias Kunze
51c6d60760 Use http_date instead of cookie_date (#1042)
http_date is deprecated as of Django 2.1
2018-10-09 10:50:25 +02:00
Raphael Michel
db513b21f8 Fix Apple Pay verification for organizer domains 2018-10-09 09:22:12 +02:00
Raphael Michel
ab336678ce Allow to change slug in admin sessions 2018-10-09 09:19:36 +02:00
Raphael Michel
3eea4d6945 Show suebvent in addons view 2018-10-08 12:17:30 +02:00
Raphael Michel
d091d3fd17 Show subevent in questions form 2018-10-08 11:20:15 +02:00
Raphael Michel
fc71f484ad Fix urlconf definition 2018-10-05 10:41:35 +02:00
Raphael Michel
bd772bf900 Never fail to send an email because of missing attachments 2018-10-05 09:33:40 +02:00
Raphael Michel
14db654681 Fix Apple Pay for custom domains 2018-10-05 09:31:23 +02:00
Raphael Michel
a85b96ea89 Allow plugins to have organizer_patterns 2018-10-05 09:31:14 +02:00
Raphael Michel
c2b5e876bc Bump version to 2.2.0.dev0 2018-10-04 11:35:00 +02:00
Raphael Michel
91c02dc0b3 Bump version to 2.1.0 2018-10-04 11:33:09 +02:00
Raphael Michel
f78ec830b5 Fix pretix-stripe.js 2018-10-03 17:31:06 +02:00
Raphael Michel
9f0e508ab3 Do not require meta_noindex 2018-10-03 12:52:37 +02:00
Raphael Michel
4ca50d750b Merge pull request #1037 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-10-03 12:44:14 +02:00
Raphael Michel
07c1b1b7f3 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-03 10:43:50 +00:00
Raphael Michel
3e95dd52cf Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-03 10:43:35 +00:00
Raphael Michel
80ef2f6b0e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2773 of 2773 strings)

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

powered by weblate
2018-10-03 10:38:42 +00:00
Raphael Michel
53a8cda310 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-10-03 12:25:02 +02:00
Raphael Michel
63de49104c Merge pull request #1016 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-10-03 12:24:28 +02:00
Maarten van den Berg
8aa80bcb84 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (2727 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
oocf
95115a7c5e Translated on translate.pretix.eu (Spanish)
Currently translated at 99.9% (2725 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
oocf
ce2967fd02 Translated on translate.pretix.eu (Spanish)
Currently translated at 99.9% (2725 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
oocf
399fb87d20 Translated on translate.pretix.eu (Spanish)
Currently translated at 99.7% (2719 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
oocf
c4bd5ac5df Translated on translate.pretix.eu (Spanish)
Currently translated at 99.7% (2719 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
Maarten van den Berg
123c2d6c02 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.4% (2711 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
Maarten van den Berg
6954e9c984 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (65 of 65 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
Yunus Fırat Pişkin
fc573e4e48 Translated on translate.pretix.eu (Turkish)
Currently translated at 100.0% (2727 of 2727 strings)

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

powered by weblate
2018-10-03 10:15:55 +00:00
Raphael Michel
0dbcfdc5ac Allow to enable ticket downloads for pending orders 2018-10-03 12:15:43 +02:00
Raphael Michel
4b8d4b4792 Allow to bulk-delete vouchers 2018-10-03 11:32:55 +02:00
Raphael Michel
d798da33ef Add option to add robots=noindex meta tag 2018-10-03 11:15:59 +02:00
Raphael Michel
d99517c8d1 Fix #917 -- Attach tickets to emails (#1034) 2018-10-03 11:06:50 +02:00
Raphael Michel
0787adcb8e Fix AttributeError in paypal module 2018-10-02 17:12:26 +02:00
Raphael Michel
f848561d25 Expose log details for admins 2018-10-01 14:13:44 +02:00
Raphael Michel
efbddc2486 Log failed payments 2018-10-01 13:48:47 +02:00
Raphael Michel
e6a138d8f2 Bank transfer: Use correct attribute 2018-10-01 13:05:17 +02:00
Raphael Michel
5b7a578307 Improve display of stripe transaction data 2018-10-01 12:47:36 +02:00
Raphael Michel
737738de93 Fix control display of bank transfers 2018-10-01 12:43:12 +02:00
Raphael Michel
eb3951ce13 Fix waiting list action view without return value 2018-10-01 12:43:12 +02:00
Raphael Michel
c2b7d9a257 Fix transaction handling in invite form 2018-09-30 14:07:14 +02:00
Raphael Michel
4738aa2771 Fix contextual table styles 2018-09-30 13:11:33 +02:00
Raphael Michel
29ac0af55e Improve Device.__str__ method 2018-09-28 16:33:15 +02:00
Raphael Michel
96bc64c456 Do not break invoices if order has no locale 2018-09-27 17:15:49 +02:00
Raphael Michel
0369deb72d Fix permission for access to root event resource 2018-09-27 10:01:57 +02:00
Raphael Michel
6e53990845 Make last commit more resilient 2018-09-25 18:20:40 +02:00
Raphael Michel
feb262644e Orders API: Reduce query load imposed by ?pdf_data=true by multiple orders of magnitude 2018-09-25 17:39:58 +02:00
Raphael Michel
abd679820f Merge pull request #1017 from pretix/deviceauth
Authentication scheme for devices
2018-09-25 14:36:23 +02:00
Raphael Michel
cd3ce848d1 Document permissions 2018-09-25 12:30:15 +02:00
Raphael Michel
63ba393c12 Proper permission handling and testing 2018-09-25 12:29:05 +02:00
Raphael Michel
23fdf8c457 Add compatibility note 2018-09-25 12:12:33 +02:00
Raphael Michel
304ad4e3db Restrict list of events 2018-09-25 10:54:36 +02:00
Raphael Michel
ec58ab07b6 Add tests for control 2018-09-25 10:28:07 +02:00
Raphael Michel
1ba4047b1b API-level tests 2018-09-25 10:28:07 +02:00
Raphael Michel
0bab8adc41 Add documentation on auth 2018-09-25 10:28:07 +02:00
Raphael Michel
17e09c601e Revoke + Logging 2018-09-25 10:28:07 +02:00
Raphael Michel
1aca5fb6ff Fix wrong action parameter 2018-09-25 10:28:07 +02:00
Raphael Michel
7860d690fa Add endpoints to update, roll and revoke devices 2018-09-25 10:28:07 +02:00
Raphael Michel
6d01c99d38 Auth mechanism 2018-09-25 10:28:07 +02:00
Raphael Michel
ddb645aeea Creating device objects 2018-09-25 10:28:07 +02:00
Raphael Michel
f08e4b41c4 Data model 2018-09-25 10:28:07 +02:00
Raphael Michel
1e23624955 Fix #1032 -- Workaround for markdown version 2018-09-24 14:07:11 +02:00
Raphael Michel
ee951a7448 API: Add subevent list on organizer level 2018-09-24 12:59:44 +02:00
Raphael Michel
9935ba370d Event list API: Do not show events without any access permissions 2018-09-24 12:44:45 +02:00
Raphael Michel
e815cce143 Event list API: Add filters 2018-09-24 12:36:12 +02:00
Raphael Michel
cea1032180 SplitDateTimeField: Adjust placeholders to actual locale 2018-09-21 16:54:22 +02:00
Raphael Michel
5695e1d9c8 SplitDateTimeField: Consider field empty if only a time is given 2018-09-21 16:54:22 +02:00
Raphael Michel
fd317afd01 Improve accessibility of payment selection 2018-09-21 16:54:22 +02:00
Raphael Michel
ccddd2a96f Activate passbook by default if installed 2018-09-21 16:54:22 +02:00
Raphael Michel
513d3034d8 Remove deprecated template part 2018-09-20 21:12:49 +02:00
Raphael Michel
51495187fa Merge pull request #1028 from chrko/error_pages_html
Fix outside of body script element
2018-09-20 10:08:11 +02:00
Christian Kohlstedde
2bd53f7b9f Fix outside of body script element
Signed-off-by: Christian Kohlstedde <christian@kohlsted.de>
2018-09-20 10:00:55 +02:00
Raphael Michel
06d9c48ed4 Allow to restrict payment methods by invoice address country 2018-09-19 16:10:40 +02:00
Raphael Michel
1155d18b7f Show waiting list options even when waiting list is disabled 2018-09-19 15:44:17 +02:00
Raphael Michel
6e14592c78 Delete check-ins when deleting a check-in list 2018-09-19 15:41:49 +02:00
Raphael Michel
55feaf2d2c Invoices: Your reference → Customer reference 2018-09-19 15:40:50 +02:00
Raphael Michel
c487036c8b Fix bug in thumbnail generation of small images 2018-09-19 15:38:12 +02:00
Raphael Michel
853ebf8c70 Fix Sphinx warnings 2018-09-19 14:00:01 +02:00
Raphael Michel
1c695c1cf9 Remove unused resource from docs 2018-09-19 13:59:15 +02:00
Raphael Michel
bd5687d169 Remove lock when paying a pending order 2018-09-17 13:04:49 +02:00
Raphael Michel
b384f71b64 Fail silently if attachment could not be found 2018-09-13 12:58:08 +02:00
Raphael Michel
10dd5278e7 Fix bug in previous commit 2018-09-13 12:32:07 +02:00
Raphael Michel
befa6527e4 Attach invoice to order approval email 2018-09-13 12:19:30 +02:00
Raphael Michel
00497630cb Merge pull request #1015 from thegcat/patch-1
Correct typo
2018-09-12 08:38:56 +02:00
Felix Schäfer
95cd457de1 Correct typo
The Header is `Content-Type` not `Content`.
2018-09-11 22:11:48 +02:00
Raphael Michel
7518c9e3e0 Do not install python34.txt on release CI 2018-09-11 18:24:00 +02:00
Raphael Michel
6a999835e2 Bump version 2018-09-11 18:23:10 +02:00
462 changed files with 122326 additions and 38552 deletions

View File

@@ -17,7 +17,7 @@ pypi:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz

View File

@@ -11,7 +11,6 @@ fi
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
psql -c 'create database travis_ci_test;' -U postgres
pip3 install -Ur src/requirements/postgres.txt
fi
if [ "$1" == "style" ]; then
@@ -39,11 +38,11 @@ if [ "$1" == "translation-spelling" ]; then
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt pytest-xdist
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 2 tests
py.test --reruns 5 -n 3 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt

View File

@@ -1,7 +1,7 @@
language: python
sudo: false
install:
- pip install -U pip wheel setuptools==28.6.1
- pip install -U pip wheel setuptools
script:
- bash .travis.sh $JOB
cache:
@@ -18,12 +18,12 @@ matrix:
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
- python: 3.6
@@ -32,11 +32,15 @@ matrix:
env: JOB=translation-spelling
addons:
postgresql: "9.4"
mariadb: '10.3'
apt:
packages:
- enchant
- myspell-de-de
- aspell-en
- sqlite3
sources:
- travis-ci/sqlite3
branches:
except:
- /^weblate-.*/

View File

@@ -1,10 +1,26 @@
FROM python:3.6
RUN apt-get update && \
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
default-libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
--no-install-recommends && \
apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
gettext \
git \
libffi-dev \
libjpeg-dev \
libmemcached-dev \
libpq-dev \
libssl-dev \
libxml2-dev \
libxslt1-dev \
locales \
nginx \
python-dev \
python-virtualenv \
python3-dev \
sudo \
supervisor \
zlib1g-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
dpkg-reconfigure locales && \
@@ -19,6 +35,22 @@ RUN apt-get update && \
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/requirements /pretix/src/requirements
COPY src/requirements.txt /pretix/src
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
pip3 install \
-r requirements.txt \
-r requirements/memcached.txt \
-r requirements/mysql.txt \
-r requirements/redis.txt \
gunicorn && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
@@ -27,11 +59,8 @@ COPY src /pretix/src
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
pip3 install -U pip wheel setuptools && \
cd /pretix/src && \
rm -f pretix.cfg && \
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \
sudo -u pretixuser make production

View File

@@ -125,6 +125,23 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
Database replica settings
-------------------------
If you use a replicated database setup, pretix expects that the default database connection always points to the primary database node.
Routing read queries to a replica on database layer is **strongly** discouraged since this can lead to inaccurate such as more tickets
being sold than are actually available.
However, pretix can still make use of a database replica to keep some expensive queries with that can tolerate some latency from your
primary database, such as backend search queries. The ``replica`` configuration section can have the same settings as the ``database``
section (except for the ``backend`` setting) and will default back to the ``database`` settings for all values that are not given. This
way, you just need to specify the settings that are different for the replica.
Example::
[replica]
host=192.168.0.2
URLs
----
@@ -295,5 +312,13 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
; Voucher code needs to be < 255 characters, default is 16
voucher_code=16
External tools
--------------
pretix can make use of some external tools if they are installed. Currently, they are all optional. Example::
[tools]
pdftk=/usr/bin/pdftk
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html

View File

@@ -0,0 +1,37 @@
.. highlight:: none
Installing a development version
================================
If you want to use a feature of pretix that is not yet contained in the last monthly release, you can also
install a development version with pretix.
.. warning:: When in production, we strongly recommend only installing released versions. Development versions might
be broken, incompatible to plugins, or in rare cases incompatible to upgrade later on.
Manual installation
-------------------
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Docker installation
-------------------
To use the latest development version with Docker, first pull it from Docker Hub::
$ docker pull pretix/standalone:latest
Then change your ``/etc/systemd/system/pretix.service`` file to use the ``:latest`` tag instead of ``:stable`` as well
and upgrade as usual::
$ systemctl restart pretix.service
$ docker exec -it pretix.service pretix upgrade

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `MySQL`_ or `PostgreSQL`_ database server
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -36,6 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
On this guide
-------------
@@ -58,7 +61,7 @@ Next, we need a database and a database user. We can create these with any kind
our database's shell, e.g. for MySQL::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;

View File

@@ -0,0 +1,84 @@
.. highlight:: none
Installing pretix Enterprise plugins
====================================
If you want to use a feature of pretix that is part of our commercial offering pretix Enterprise, you need to follow
some extra steps. Installation works similar to normal pretix plugins, but involves a few extra steps.
Buying the license
------------------
To obtain a license, please get in touch at sales@pretix.eu. Please let us know how many tickets you roughly intend
to sell per year and how many servers you want to use the plugin on. We recommend having a look at our `price list`_
first.
Manual installation
-------------------
First, generate an SSH key for the system user that you install pretix as. In our tutorial, that would be the user
``pretix``. Choose an empty passphrase::
# su pretix
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/var/pretix/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/pretix/.ssh/id_rsa.
Your public key has been saved in /var/pretix/.ssh/id_rsa.pub.
Next, send the content of the *public* key to your sales representative at pretix::
$ cat /var/pretix/.ssh/id_rsa.pub
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
After we configured your key in our system, you can install the plugin directly using ``pip`` from the URL we told
you, for example::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
# systemctl restart pretix-web pretix-worker
Docker installation
-------------------
To install a plugin, you need to build your own docker image. To do so, create a new directory to work in. As a first
step, generate a new SSH key in that directory to use for authentication with us::
$ cd /home/me/mypretixdocker
$ ssh-keygen -N "" -f id_pretix_enterprise
Next, send the content of the *public* key to your sales representative at pretix::
$ cat id_pretix_enterprise.pub
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
After we configured your key in our system, you can add a ``Dockerfile`` in your directory that includes the newly
generated key and installs the plugin from the URL we told you::
FROM pretix/standalone:stable
USER root
COPY id_pretix_enterprise /root/.ssh/id_rsa
COPY id_pretix_enterprise.pub /root/.ssh/id_rsa.pub
RUN chmod -R 0600 /root/.ssh && \
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser
Then, build the image for docker::
$ docker build -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone:stable`` in your ``/etc/systemd/system/pretix.service``
service file. Be sure to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an
update to a new version of pretix.
.. _price list: https://pretix.eu/about/en/pricing

View File

@@ -21,6 +21,9 @@ To use pretix, you will need the following things:
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -10,3 +10,5 @@ for your needs.
general
docker_smallscale
manual_smallscale
dev_version
enterprise

View File

@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `MySQL`_ or `PostgreSQL`_ database server
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -33,6 +33,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
Unix user
---------
@@ -50,7 +53,7 @@ Having the database server installed, we still need a database and a database us
of database managing tool or directly on our database's shell, e.g. for MySQL::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
@@ -61,7 +64,7 @@ To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libmysqlclient-dev libjpeg-dev
gettext libpq-dev libmysqlclient-dev libjpeg-dev libopenjp2-7-dev
Config file
-----------

9
doc/api/auth.rst Normal file
View File

@@ -0,0 +1,9 @@
Authentication
==============
.. toctree::
:maxdepth: 2
tokenauth
oauth
deviceauth

137
doc/api/deviceauth.rst Normal file
View File

@@ -0,0 +1,137 @@
.. _`rest-deviceauth`:
Device authentication
=====================
Initializing a new device
-------------------------
Users can create new devices in the "Device" section of their organizer settings. When creating
a new device, users can specify a list of events the device is allowed to access. After a new
device is created, users will be presented initialization instructions, consisting of an URL
and an initialization token. They will also be shown as a QR code with the following contents::
{"handshake_version": 1, "url": "https://pretix.eu", "token": "kpp4jn8g2ynzonp6"}
Your application should be able to scan a QR code of this type, or allow to enter the URL and the
initialization token manually. The handshake version is not used for manual initialization. When a
QR code is scanned with a higher handshake version than you support, you should reject the request
and prompt the user to update the client application.
After your application received the token, you need to call the initialization endpoint to obtain
a proper API token. At this point, you need to identify the name and version of your application,
as well as the type of underlying hardware. Example:
.. sourcecode:: http
POST /api/v1/device/initialize HTTP/1.1
Host: pretix.eu
Content-Type: application/json
{
"token": "kpp4jn8g2ynzonp6",
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"software_brand": "pretixdroid",
"software_version": "4.0.0"
}
Every initialization token can only be used once. On success, you will receive a response containing
information on your device as well as your API token:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"organizer": "foo",
"device_id": 5,
"unique_serial": "HHZ9LW9JWP390VFZ",
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
"name": "Bar"
}
Please make sure that you store this ``api_token`` value. We also recommend storing your device ID, your assigned
``unique_serial``, and the ``organizer`` you have access to, but that's up to you.
In case of an error, the response will look like this:
.. sourcecode:: http
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"token":["This initialization token has already been used."]}
Performing API requests
-----------------------
You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:
.. sourcecode:: http
:emphasize-lines: 3
GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
Updating the software version
-----------------------------
If your application is updated, we ask you to tell the server about the new version in use. You can do this at the
following endpoint:
.. sourcecode:: http
POST /api/v1/device/update HTTP/1.1
Host: pretix.eu
Content-Type: application/json
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
{
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"software_brand": "pretixdroid",
"software_version": "4.1.0"
}
Creating a new API key
----------------------
If you think your API key might have leaked or just want to be extra cautious, the API allows you to create a new key.
The old API key will be invalid immediately. A request for a new key looks like this:
.. sourcecode:: http
POST /api/v1/device/roll HTTP/1.1
Host: pretix.eu
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
The response will look like the response to the initialization request.
Removing a device
-----------------
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
invalidate your API key. There is no way to reverse this operation.
.. sourcecode:: http
POST /api/v1/device/revoke HTTP/1.1
Host: pretix.eu
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
This can also be done by the user through the web interface.
Permissions
-----------
Device authentication is currently hardcoded to grant the following permissions:
* View event meta data and products etc.
* View and change orders
Devices cannot change events or products and cannot access vouchers.

View File

@@ -9,44 +9,20 @@ with pretix' REST API, such as authentication, pagination and similar definition
Authentication
--------------
If you're building an application for end users, we strongly recommend that you use our
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
can also go with static API tokens that you can create on a per-team basis (see below).
To access the API, you need to present valid authentication credentials. pretix currently
supports the following authorization schemes:
You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:
.. sourcecode:: http
:emphasize-lines: 3
GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
.. note:: The API currently also supports authentication via browser sessions, i.e. the
same way that you authenticate with pretix when using the browser interface.
Using this type of authentication is *not* officially supported for use by
third-party clients and might change or be removed at any time. We plan on
adding OAuth2 support in the future for user-level authentication. If you want
to use session authentication, be sure to comply with Django's `CSRF policies`_.
Obtaining an API token
----------------------
To authenticate your API requests, you need to obtain an API token. You can create a
token in the pretix web interface on the level of organizer teams. Create a new team
or choose an existing team that has the level of permissions the token should have and
create a new token using the form below the list of team members:
.. image:: img/token_form.png
:class: screenshot
You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.
.. image:: img/token_success.png
:class: screenshot
* :ref:`rest-tokenauth`: This is the simplest way and recommended for server-side applications
that interact with pretix without user interaction.
* :ref:`rest-oauth`: This is the recommended way to use if you write a third-party application
that users can connect with their pretix account. It provides the best user experience, but
requires user interaction and slightly more implementation effort.
* :ref:`rest-deviceauth`: This is the recommended way if you build apps or hardware devices that can
connect to pretix, e.g. for processing check-ins or to sell tickets offline. It provides a way
to uniquely identify devices and allows for a quick configuration flow inside your software.
* Authentication using browser sessions: This is used by the pretix web interface and it is *not*
officially supported for use by third-party applications. It might change or be removed at any
time without prior notice. If you use it, you need to comply with Django's `CSRF policies`_.
Permissions
-----------
@@ -172,6 +148,7 @@ Field specific input errors include the name of the offending fields as keys in
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
Data types
----------
@@ -204,4 +181,4 @@ as the string values ``true`` and ``false``.
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
fields. Prepend a ``-`` to the field name to reverse the sort order.
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax

View File

@@ -14,5 +14,7 @@ in functionality over time.
:maxdepth: 2
fundamentals
oauth
auth
resources/index
ratelimit
webhooks

View File

@@ -1,7 +1,7 @@
.. _`rest-oauth`:
OAuth support / "Connect with pretix"
=====================================
OAuth authentication / "Connect with pretix"
============================================
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
@@ -166,6 +166,42 @@ endpoint to revoke it.
If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
pretix user interface.
Fetching the user profile
-------------------------
If you need the user's meta data, you can fetch it here:
.. http:get:: /api/v1/me
Returns the profile of the authenticated user
**Example request**:
.. sourcecode:: http
GET /api/v1/me HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
email: "admin@localhost",
fullname: "John Doe",
locale: "de",
timezone: "Europe/Berlin"
}
:statuscode 200: no error
:statuscode 401: Authentication failure
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication

31
doc/api/ratelimit.rst Normal file
View File

@@ -0,0 +1,31 @@
.. _`rest-ratelimit`:
Rate limiting
=============
.. note:: This page only applies to the pretix Hosted service at pretix.eu. APIs of custom pretix installations do not
enforce any rate limiting by default.
All authenticated requests to pretix' API are rate limited. If you exceed the limits, you will receive a response
with HTTP status code ``429 Too Many Requests``. This response will have a ``Retry-After`` header, containing the number
of seconds you are supposed to wait until you try again. We expect that all API clients respect this. If you continue
to burst requests after a ``429`` status code, we might get in touch with you or, in extreme cases, disable your API
access.
Currently, the following rate limits apply:
.. rst-class:: rest-resource-table
===================================== =================================================================================
Authentication method Rate limit
===================================== =================================================================================
:ref:`rest-deviceauth` 360 requests per minute per device
:ref:`rest-tokenauth` 360 requests per minute per organizer account
:ref:`rest-oauth` 360 requests per minute per combination of accessed organizer and OAuth application
Session authentication *Not an officially supported authentication method for external access*
===================================== =================================================================================
If you require a higher rate limit, please get in touch at support@pretix.eu and tell us about your use case, we are
sure we can work something out.

View File

@@ -25,6 +25,7 @@ item integer ID of the item
variation integer ID of the variation (or ``null``)
price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Composition of attendee name (i.e. first name, last name, …)
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
@@ -78,6 +79,7 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -122,6 +124,7 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -175,7 +178,7 @@ Cart position endpoints
* ``item``
* ``variation`` (optional)
* ``price``
* ``attendee_name`` (optional)
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
@@ -199,7 +202,10 @@ Cart position endpoints
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"given_name": "Peter",
"family_name": "Miller"
},
"attendee_email": null,
"answers": [
{

View File

@@ -371,6 +371,9 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -442,6 +445,8 @@ Order position endpoints
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
``checkins`` value will only include check-ins for the selected list.
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
**Example request**:
.. sourcecode:: http
@@ -466,6 +471,9 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -510,6 +518,8 @@ Order position endpoints
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
accepts a number of optional requests in the body.
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
@@ -555,7 +565,10 @@ Order position endpoints
Content-Type: application/json
{
"status": "ok"
"status": "ok",
"position": {
}
}
**Example response with required questions**:
@@ -566,7 +579,10 @@ Order position endpoints
Content-Type: text/json
{
"status": "incomplete"
"status": "incomplete",
"position": {
},
"questions": [
{
"id": 1,
@@ -611,6 +627,9 @@ Order position endpoints
{
"status": "error",
"reason": "unpaid",
"position": {
}
}
Possible error reasons:

View File

@@ -15,6 +15,7 @@ name multi-lingual string The event's ful
slug string A short form of the name, used e.g. in URLs.
live boolean If ``true``, the event ticket shop is publicly
available.
testmode boolean If ``true``, the ticket shop is in test mode.
currency string The currency this event is handled in.
date_from datetime The event's start date
date_to datetime The event's end date (or ``null``)
@@ -41,6 +42,14 @@ plugins list A list of packa
The ``plugins`` field has been added.
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 2.1
Filters have been added to the list of events.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added.
Endpoints
---------
@@ -75,6 +84,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -96,6 +106,12 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only events with a matching value of ``is_public`` are returned.
:query live: If set to ``true``/``false``, only events with a matching value of ``live`` are returned.
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -127,6 +143,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -173,6 +190,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -201,6 +219,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -249,6 +268,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -277,6 +297,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -337,6 +358,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,

View File

@@ -21,3 +21,4 @@ Resources and endpoints
checkinlists
waitinglist
carts
webhooks

View File

@@ -37,6 +37,8 @@ admission boolean ``True`` for it
position integer An integer, used for sorting
picture string A product picture to be displayed in the shop
(read-only).
sales_channels list of strings Sales channels this product is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_until datetime The last date time at which this item can be bought
@@ -62,6 +64,12 @@ original_price money (string) An original pri
require_approval boolean If ``True``, orders with this product will need to be
approved by the event organizer before they can be
paid.
generate_tickets boolean If ``False``, tickets are never generated for this
product, regardless of other settings. If ``True``,
tickets are generated even if this is a
non-admission or add-on product, regardless of event
settings. If this is ``null``, regular ticketing
rules apply.
has_variations boolean Shows whether or not this item has variations.
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
@@ -105,6 +113,14 @@ addons list of objects Definition of a
The field ``require_approval`` has been added.
.. versionchanged:: 2.3
The ``sales_channels`` attribute has been added.
.. versionchanged:: 2.4
The ``generate_tickets`` attribute has been added.
Notes
-----
Please note that an item either always has variations or never has. Once created with variations the item can never
@@ -147,6 +163,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -167,6 +184,7 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"generate_tickets": null,
"require_approval": false,
"variations": [
{
@@ -232,6 +250,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -248,6 +267,7 @@ Endpoints
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"generate_tickets": null,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
@@ -298,6 +318,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -314,6 +335,7 @@ Endpoints
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"generate_tickets": null,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
@@ -351,6 +373,7 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -369,6 +392,7 @@ Endpoints
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"generate_tickets": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
@@ -436,6 +460,7 @@ Endpoints
"id": 1,
"name": {"en": "Ticket"},
"internal_name": "",
"sales_channels": ["web"],
"default_price": "25.00",
"original_price": null,
"category": null,
@@ -451,6 +476,7 @@ Endpoints
"available_until": null,
"require_voucher": false,
"hide_without_voucher": false,
"generate_tickets": null,
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,

View File

@@ -26,10 +26,13 @@ status string Order status, o
* ``p`` paid
* ``e`` expired
* ``c`` canceled
* ``r`` refunded
testmode boolean If ``True``, this order was created when the event was in
test mode. Only orders in test mode can be deleted.
secret string The secret contained in the link sent to the customer
email string The customer email address
locale string The locale used for communication with this customer
sales_channel string Channel this sale was created through, such as
``"web"``.
datetime datetime Time of order creation
expires datetime The order will expire, if it is still pending by this time
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
@@ -46,6 +49,7 @@ invoice_address object Invoice address
for orders created before pretix 1.7, do not rely on
it).
├ name string Customer name
├ name_parts object of strings Customer name decomposition
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
@@ -55,9 +59,9 @@ invoice_address object Invoice address
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
EU VAT service and validation was successful. This only
happens in rare cases.
positions list of objects List of order positions (see below)
fees list of objects List of fees included in the order total (i.e.
payment fees)
positions list of objects List of non-canceled order positions (see below)
fees list of objects List of non-canceled fees included in the order total
(i.e. payment fees)
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
``other``)
├ value money (string) Fee amount
@@ -120,6 +124,19 @@ last_modified datetime Last modificati
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. versionchanged:: 2.3
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4:
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5:
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. _order-position-resource:
Order position resource
@@ -137,6 +154,7 @@ item integer ID of the purch
variation integer ID of the purchased variation (or ``null``)
price money (string) Price of this position
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``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
@@ -260,9 +278,11 @@ List of all orders
{
"code": "ABC12",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"last_modified": "2017-12-01T10:00:00Z",
@@ -278,6 +298,7 @@ List of all orders
"is_business": True,
"company": "Sample company",
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -295,6 +316,9 @@ List of all orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -353,11 +377,14 @@ List of all orders
``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.
:query string email: Only return orders created with the given email address
:query string locale: Only return orders with the given customer locale
:query datetime modified_since: Only return orders that have changed since the given date
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -392,9 +419,11 @@ Fetching individual orders
{
"code": "ABC12",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"last_modified": "2017-12-01T10:00:00Z",
@@ -410,6 +439,7 @@ Fetching individual orders
"company": "Sample company",
"is_business": True,
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -427,6 +457,9 @@ Fetching individual orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -530,6 +563,37 @@ Order ticket download
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Deleting orders
---------------
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
Deletes an order. Works only if the order has ``testmode`` set to ``true``.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
Content-Type: application/json
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
:statuscode 404: The requested order does not exist.
Creating orders
---------------
@@ -551,6 +615,8 @@ Creating orders
* does not validate if products are only to be sold in a specific time frame
* does not validate if products are only to be sold on other sales channels
* does not validate if the event's ticket sales are already over or haven't started
* does not validate the number of items per order or the number of times an item can be included in an order
@@ -582,11 +648,13 @@ Creating orders
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
**not** be sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and
then call the ``mark_paid`` API method.
* ``testmode`` (optional) Defaults to ``false``
* ``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 that become free by this operation will be credited to your order
creation.
* ``email``
* ``locale``
* ``sales_channel``
* ``payment_provider`` 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.
@@ -601,7 +669,7 @@ Creating orders
* ``company``
* ``is_business``
* ``name``
* ``name`` **or** ``name_parts``
* ``street``
* ``zipcode``
* ``city``
@@ -615,7 +683,7 @@ Creating orders
* ``item``
* ``variation``
* ``price``
* ``attendee_name``
* ``attendee_name`` **or** ``attendee_name_parts``
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -651,6 +719,7 @@ Creating orders
{
"email": "dummy@example.org",
"locale": "en",
"sales_channel": "web",
"fees": [
{
"fee_type": "payment",
@@ -664,7 +733,7 @@ Creating orders
"invoice_address": {
"is_business": False,
"company": "Sample company",
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
@@ -678,7 +747,9 @@ Creating orders
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter"
},
"attendee_email": null,
"addon_to": null,
"answers": [
@@ -752,7 +823,10 @@ Order state operations
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_canceled/
Marks a pending order as canceled.
Cancels an order. For a pending order, this will set the order to status ``c``. For a paid order, this will set
the order to status ``c`` if no ``cancellation_fee`` is passed. If you do pass a ``cancellation_fee``, the order
will instead stay paid, but all positions will be removed (or marked as canceled) and replaced by the cancellation
fee as the only component of the order.
**Example request**:
@@ -764,7 +838,8 @@ Order state operations
Content-Type: text/json
{
"send_email": true
"send_email": true,
"cancellation_fee": null
}
**Example response**:
@@ -825,44 +900,6 @@ 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)/mark_refunded/
Marks a paid order as refunded.
.. warning:: In the current implementation, this will **bypass** the payment provider, i.e. the money will **not** be
transferred back to the user automatically, the order will only be *marked* as refunded within pretix.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_expired/ 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": "r",
...
}
: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 marked as expired since the current order status does not allow it.
: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_expired/
Marks a unpaid order as expired.
@@ -1042,6 +1079,8 @@ List of all order positions
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. note:: Individually canceled order positions are currently not visible via the API at all.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
Returns a list of all order positions within a given event.
@@ -1075,6 +1114,9 @@ List of all order positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter"
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -1172,6 +1214,9 @@ Fetching individual positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -1471,7 +1516,7 @@ Order payment endpoints
{
"amount": "23.00",
"mark_refunded": false
"mark_canceled": false
}
@@ -1618,7 +1663,7 @@ Order refund endpoints
"payment": 1,
"execution_date": null,
"provider": "manual",
"mark_refunded": false
"mark_canceled": false
}
**Example response**:
@@ -1688,7 +1733,7 @@ Order refund endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/process/
Acts on an external refund, either marks the order as refunded or pending. Only allowed in state ``external``.
Acts on an external refund, either marks the order as canceled or pending. Only allowed in state ``external``.
**Example request**:
@@ -1699,7 +1744,7 @@ Order refund endpoints
Accept: application/json, text/javascript
Content-Type: application/json
{"mark_refunded": false}
{"mark_canceled": false}
**Example response**:

View File

@@ -128,7 +128,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"identifier": "LVETRWVU",

View File

@@ -17,6 +17,7 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the sub-event
name multi-lingual string The sub-event's full name
event string The slug of the parent event
active boolean If ``true``, the sub-event ticket shop is publicly
available.
date_from datetime The sub-event's start date
@@ -40,6 +41,10 @@ meta_data dict Values set for
The ``meta_data`` field has been added.
.. versionchanged:: 2.1
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
Endpoints
---------
@@ -72,6 +77,7 @@ Endpoints
{
"id": 1,
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -92,6 +98,10 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -121,6 +131,7 @@ Endpoints
{
"id": 1,
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -144,3 +155,63 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/subevents/
Returns a list of all sub-events of any event series you have access to within an organizer account.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/subevents/ 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,
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -231,6 +231,76 @@ Endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/vouchers/batch_create/
Creates multiple new vouchers atomically.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/vouchers/batch_create/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 408
[
{
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"valid_until": null,
"block_quota": false,
"allow_ignore_quota": false,
"price_mode": "set",
"value": "12.00",
"item": 1,
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
},
{
"code": "ASDKLJCYXCASDASD",
"max_usages": 1,
"valid_until": null,
"block_quota": false,
"allow_ignore_quota": false,
"price_mode": "set",
"value": "12.00",
"item": 1,
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
},
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
[
{
"id": 1,
"code": "43K6LKM37FBVR2YG",
}, …
}
:param organizer: The ``slug`` field of the organizer to create a vouchers for
:param event: The ``slug`` field of the event to create a vouchers for
:statuscode 201: no error
:statuscode 400: The vouchers 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 this resource.
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of

View File

@@ -0,0 +1,242 @@
.. _`rest-webhooks`:
Webhooks
========
.. note:: This page is about how to modify webhook settings themselves through the REST API. If you just want to know
how webhooks work, go here: :ref:`webhooks`
Resource description
--------------------
The webhook resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the webhook
enabled boolean If ``False``, this webhook will not receive any notifications
target_url string The URL to call
all_events boolean If ``True``, this webhook will receive notifications
on all events of this organizer
limit_events list of strings If ``all_events`` is ``False``, this is a list of
event slugs this webhook is active for
action_types list of strings A list of action type filters that limit the
notifications sent to this webhook. See below for
valid values
===================================== ========================== =======================================================
The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.placed``
* ``pretix.event.order.paid``
* ``pretix.event.order.canceled``
* ``pretix.event.order.expired``
* ``pretix.event.order.modified``
* ``pretix.event.order.contact.changed``
* ``pretix.event.order.changed.*``
* ``pretix.event.order.refund.created.externally``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.checkin``
* ``pretix.event.checkin.reverted``
Installed plugins might register more valid values.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/webhooks/
Returns a list of all webhooks within a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/webhooks/ 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": 2,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/webhooks/(id)/
Returns information on one webhook, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/webhooks/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": 2,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the webhook to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/webhooks/
Creates a new webhook
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 3,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to create a webhook for
:statuscode 201: no error
:statuscode 400: The webhook could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/webhooks/(id)/
Update a webhook. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"enabled": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": false,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the webhook to modify
:statuscode 200: no error
:statuscode 400: The webhook could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/webhook/(id)/
Delete a webhook. Currently, this will not delete but just disable the webhook.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/webhooks/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 id: The ``id`` field of the webhook to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.

36
doc/api/tokenauth.rst Normal file
View File

@@ -0,0 +1,36 @@
.. _`rest-tokenauth`:
Token-based authentication
==========================
Obtaining an API token
----------------------
To authenticate your API requests with Tokens, you need to obtain a team-level API token.
You can create a token in the pretix web interface on the level of organizer teams. Create
a new team or choose an existing team that has the level of permissions the token should
have and create a new token using the form below the list of team members:
.. image:: img/token_form.png
:class: screenshot
You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.
.. image:: img/token_success.png
:class: screenshot
Using an API token
------------------
You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:
.. sourcecode:: http
:emphasize-lines: 3
GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k

108
doc/api/webhooks.rst Normal file
View File

@@ -0,0 +1,108 @@
.. _`webhooks`:
Webhooks
========
pretix can send webhook calls to notify your application of any changes that happen inside pretix. This is especially
useful for everything triggered by an actual user, such as a new ticket sale or the arrival of a payment.
You can register any number of webhook URLs that pretix will notify any time one of the supported events occurs inside
your organizer account. A great example use case of webhooks would be to add the buyer to your mailing list every time
a new order comes in.
Configuring webhooks
--------------------
You can find the list of your active webhooks in the "Webhook" section of your organizer account:
.. thumbnail:: ../screens/organizer/webhook_list.png
:align: center
:class: screenshot
Click "Create webhook" if you want to add a new URL. You will then be able to enter the URL pretix shall call for
notifications. You need to select any number of notification types that you want to receive and you can optionally
filter the events you want to receive notifications for.
.. thumbnail:: ../screens/organizer/webhook_edit.png
:align: center
:class: screenshot
You can also configure webhooks :ref:`through the API itself <rest-webhooks>`.
Receiving webhooks
------------------
Creating a webhook endpoint on your server is no different from creating any other page on your website. If your
website is written in PHP, you might just create a new ``.php`` file on your server; if you use a web framework like
Symfony or Django, you would just create a new route with the desired URL.
We will call your URL with a HTTP ``POST`` request with a ``JSON`` body. In PHP, you can parse this like this::
$input = @file_get_contents('php://input');
$event_json = json_decode($input);
// Do something with $event_json
In Django, you would create a view like this::
def my_webhook_view(request):
event_json = json.loads(request.body)
# Do something with event_json
return HttpResponse(status=200)
More samples for the language of your choice are easy to find online.
The exact body of the request varies by notification type, but for the main types included with pretix core, such as
those related to changes of an order, it will look like this::
{
"notification_id": 123455,
"organizer": "acmecorp",
"event": "democon",
"code": "ABC23",
"action": "pretix.event.order.placed"
}
Notifications regarding a check-in will contain more details like ``orderposition_id``
and ``checkin_list``.
.. warning:: You should not trust data supplied to your webhook, but only use it as a trigger to fetch updated data.
Anyone could send data there if they guess the correct URL and you won't be able to tell. Therefore, we
only include the minimum amount of data necessary for you to fetch the changed objects from our
:ref:`rest-api` in an authenticated way.
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require
that a valid certificate is in use.
.. note:: If you use a web framework that makes use of automatic CSRF protection, this protection might prevent us
from calling your webhook URL. In this case, we recommend that you turn of CSRF protection selectively
for that route. In Django, you can do this by putting the ``@csrf_exempt`` decorator on your view. In
Rails, you can pass an ``except`` parameter to ``protect_from_forgery``.
Responding to a webhook
-----------------------
If you successfully received a webhook call, your endpoint should return a HTTP status code between ``200`` and ``299``.
If any other status code is returned, we will assume you did not receive the call. This does mean that any redirection
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
headers and pretix will ignore all other information in your response headers or body.
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
multiple times for the same event due to a perceived error does not do any harm.
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
endpoint does not exist any more and automatically disable the webhook.
.. note:: If you use a self-hosted version of pretix (i.e. not our SaaS offering at pretix.eu) and you did not
configure a background task queue, failed webhooks will not be retried.
Debugging webhooks
------------------
If you want to debug your webhooks, you can view a log of all sent notifications and the responses of your server for
30 days right next to your configuration.
.. _Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication

View File

@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data
item_copy_data, register_sales_channels
Order events
""""""""""""
@@ -26,7 +26,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional
.. automodule:: pretix.presale.signals
@@ -49,7 +49,7 @@ Backend
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
.. automodule:: pretix.base.signals

View File

@@ -23,7 +23,7 @@ that we'll provide in this plugin::
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
def register_infoice_renderers(sender, **kwargs):
def register_invoice_renderers(sender, **kwargs):
from .invoice import MyInvoiceRenderer
return MyInvoiceRenderer

View File

@@ -64,6 +64,8 @@ The provider class
.. autoattribute:: settings_form_fields
.. automethod:: settings_form_clean
.. automethod:: settings_content_render
.. automethod:: is_allowed
@@ -96,8 +98,6 @@ The provider class
.. automethod:: order_change_allowed
.. automethod:: order_can_retry
.. automethod:: payment_prepare
.. automethod:: payment_control_render
@@ -114,6 +114,8 @@ The provider class
.. autoattribute:: is_meta
.. autoattribute:: test_mode_message
Additional views
----------------

View File

@@ -79,6 +79,9 @@ human-readable error messages. We recommend using the ``django.utils.functional.
decorator, as it might get called a lot. You can also implement ``compatibility_warnings``,
those will be displayed but not block the plugin execution.
The ``AppConfig`` class may implement a method ``is_available(event)`` that checks if a plugin
is available for a specific event. If not, it will not be shown in the plugin list of that event.
Plugin registration
-------------------

View File

@@ -82,6 +82,12 @@ Orders
^^^^^^
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
An order can be in one of currently five states that are listed in the diagram below:
An order can be in one of currently four states that are listed in the diagram below:
.. image:: /images/order_states.png
There are additional "fake" states that are displayed like states but not represented as states in the system:
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.
* An order is considered **requiring approval** if it is in **pending** status with the ``require_approval`` attribute set to ``True``.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -4,7 +4,6 @@ Pending: Order is expecting payment\nOrder reduces quotas
Expired: Payment period is over\nOrder does not affect quotas
Paid: Order was successful\nOrder reduces quotas
Canceled: Order has been canceled\nOrder does not affect quotas
Refunded: Order has been refunded\nOrder does not affect quotas
[*] --> Pending: customer\nplaces order
Pending --> Paid: successful payment
@@ -12,8 +11,9 @@ Pending --> Expired: automatically\nor manually\non admin action
Expired --> Paid: if payment is received\nonly if quota left
Expired --> Canceled
Expired --> Pending: manually\non admin action
Paid --> Refunded: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
Paid --> Canceled: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
Pending --> Canceled: on admin or\ncustomer action
Paid -> Pending: manually on admin action
[*] --> Paid: customer\nplaces free order
@enduml

View File

@@ -20,6 +20,7 @@ default boolean ``true`` if thi
layout object Layout specification for libpretixprint
background URL Background PDF file
item_assignments list of objects Products this layout is assigned to
├ sales_channel string Sales channel (defaults to ``web``).
└ item integer Item ID
===================================== ========================== =======================================================
@@ -27,6 +28,10 @@ item_assignments list of objects Products this l
This resource has been added.
.. versionchanged:: 2.3
The ``item_assignments.sales_channel`` field has been added.
Endpoints
---------

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,10 +1,12 @@
addon
addons
Analytics
anonymize
api
auditability
auth
autobuild
availabilities
backend
backends
banktransfer
@@ -23,6 +25,7 @@ cronjob
cryptographic
debian
deduplication
deprovision
discoverable
django
dockerfile
@@ -64,6 +67,7 @@ ons
optimizations
overpayment
param
passphrase
percental
positionid
pre
@@ -88,6 +92,7 @@ regex
renderer
renderers
reportlab
SaaS
screenshot
selectable
serializers
@@ -104,7 +109,9 @@ subevent
subevents
submodule
subpath
Symfony
systemd
testmode
testutils
timestamp
tuples

View File

@@ -149,8 +149,101 @@ Just as the widget, the button supports the optional attributes ``voucher`` and
You can style the button using the ``pretix-button`` CSS class.
.. versionchanged:: 1.13
Dynamically loading the widget
------------------------------
The pretix Button has been added in version 1.13.
If you need to control the way or timing the widget loads, for example because you want to modify user data (see
below) dynamically via JavaScript, you can register a listener that we will call before creating the widget::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
// Will be run before we create the widget.
}
</script>
If you want, you can suppress us loading the widget and/or modify the user data passed to the widget::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.PretixWidget.widget_data["email"] = "test@example.org";
}
</script>
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
Passing user data to the widget
-------------------------------
If you display the widget in a restricted area of your website and you want to pre-fill fields in the checkout process
with known user data to save your users some typing and increase conversions, you can pass additional data attributes
with that information::
<pretix-widget event="https://pretix.eu/demo/democon/"
data-attendee-name-given-name="John"
data-attendee-name-family-name="Doe"
data-invoice-address-name-given-name="John"
data-invoice-address-name-family-name="Doe"
data-email="test@example.org"
data-question-L9G8NG9M="Foobar">
</pretix-widget>
This works for the pretix Button as well. Currently, the following attributes are understood by pretix itself:
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
* ``data-question-IDENTIFIER`` will pre-fill the answer for the question with the given identifier. You can view and set
identifiers in the *Questions* section of the backend.
* Depending on the person name scheme configured in your event settings, you can pass one or more of
``data-attendee-name-full-name``, ``data-attendee-name-given-name``, ``data-attendee-name-family-name``,
``data-attendee-name-middle-name``, ``data-attendee-name-title``, ``data-attendee-name-calling-name``,
``data-attendee-name-latin-transcription``. If you don't know or don't care, you can also just pass a string as
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
country code.
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
made through this widget will be counted towards this campaign.
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
require you to dynamically load the widget, like this::
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXXX-1', 'auto');
ga('send', 'pageview');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if(window.ga && ga.create) {
ga(function(tracker) {
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
window.PretixWidget.buildWidgets()
});
} else { // Tracking is probably blocked
window.PretixWidget.buildWidgets()
}
});
};
</script>
.. versionchanged:: 2.3
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
fully if you configured a redis server.
.. _Let's Encrypt: https://letsencrypt.org/

View File

@@ -4,22 +4,10 @@ FAQ and Troubleshooting
How can I test my shop before taking it live?
---------------------------------------------
There are multiple ways to do this.
First, you could just create some orders in your real shop and cancel/refund them later. If you don't want to process
real payments for the tests, you can either use a "manual" payment method like bank transfer and just mark the orders
as paid with the button in the backend, or if you want to use e.g. Stripe, you can configure pretix to use your keys
for the Stripe test system and use their test credit cars. Read our :ref:`Stripe documentation <stripe>` for more
information.
Second, you could create a separate event, just for testing. In the last step of the :ref:`event creation process <event_create>`,
you can specify that you want to copy all settings from your real event, so you don't have to do all of it twice.
We are planning to add a dedicated test mode in a later version of pretix.
If you are using the hosted service at pretix.eu and want to get rid of the test orders completely, contact us at
support@pretix.eu and we can remove them for you. Please note that we only are able to do that *before* you have
received any real orders (i.e. taken the shop public). We won't charge any fees for test orders or test events.
On your event dashboard, click on the first tile that shows your shop status. On the lower part of this page, you can
place your event into "test mode". In "test mode", everything behaves the same, but orders created during test mode can
later be fully deleted. Be sure to actually delete them when or after you turn off test mode, since test mode orders
still count toward your quotas and are included in your reports.
How do I delete an event?
-------------------------

View File

@@ -1,12 +0,0 @@
[run]
source = pretix
omit = */migrations/*,*/urls.py,*/tests/*,*/testdummy/*,*/admin.py,pretix/wsgi.py,pretix/settings.py
[report]
exclude_lines =
pragma: no cover
def __str__
der __repr__
if settings.DEBUG
NOQA
NotImplementedError

View File

@@ -34,4 +34,5 @@ git push
# Unlock Weblate
for c in $COMPONENTS; do
wlc unlock $c;
wlc pull $c;
done

View File

@@ -1 +1 @@
__version__ = "2.0.0"
__version__ = "2.5.0.dev0"

View File

@@ -5,5 +5,8 @@ class PretixApiConfig(AppConfig):
name = 'pretix.api'
label = 'pretixapi'
def ready(self):
from . import signals, webhooks # noqa
default_app_config = 'pretix.api.PretixApiConfig'

View File

@@ -0,0 +1,25 @@
from django.contrib.auth.models import AnonymousUser
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.base.models import Device
class DeviceTokenAuthentication(TokenAuthentication):
model = Device
keyword = 'Device'
def authenticate_credentials(self, key):
model = self.get_model()
try:
device = model.objects.select_related('organizer').get(api_token=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
if not device.initialized:
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if not device.api_token:
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device

View File

@@ -1,7 +1,7 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Event
from pretix.base.models import Device, Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
@@ -9,10 +9,9 @@ from pretix.helpers.security import (
class EventPermission(BasePermission):
model = TeamAPIToken
def has_permission(self, request, view):
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
return False
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
@@ -31,7 +30,7 @@ class EventPermission(BasePermission):
except SessionReauthRequired:
return False
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
request.event = Event.objects.filter(
@@ -76,7 +75,7 @@ class EventCRUDPermission(EventPermission):
return False
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
return False
elif view.action in ['retrieve', 'update', 'partial_update'] \
elif view.action in ['update', 'partial_update'] \
and 'can_change_event_settings' not in request.eventpermset:
return False

View File

@@ -10,7 +10,10 @@ def custom_exception_handler(exc, context):
if isinstance(exc, LockTimeoutException):
response = Response(
{'detail': 'The server was too busy to process your request. Please try again.'},
status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT,
headers={
'Retry-After': 5
}
)
return response

View File

@@ -0,0 +1,79 @@
# Generated by Django 2.1.1 on 2018-11-07 10:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0102_auto_20181017_0024'),
('pretixapi', '0002_auto_20180604_1120'),
]
operations = [
migrations.CreateModel(
name='WebHook',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('enabled', models.BooleanField(default=True, verbose_name='Enable webhook')),
('target_url', models.URLField(verbose_name='Target URL')),
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer')),
],
),
migrations.CreateModel(
name='WebHookCall',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('target_url', models.URLField()),
('is_retry', models.BooleanField(default=False)),
('execution_time', models.FloatField(null=True)),
('return_code', models.PositiveIntegerField(default=0)),
('payload', models.TextField()),
('response_body', models.TextField()),
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
],
),
migrations.CreateModel(
name='WebHookEventListener',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
],
),
migrations.AddField(
model_name='webhookcall',
name='success',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='webhook',
name='all_events',
field=models.BooleanField(default=True, verbose_name='All events (including newly created ones)'),
),
migrations.AlterField(
model_name='webhook',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='pretixbase.Organizer'),
),
migrations.AlterField(
model_name='webhookcall',
name='webhook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='pretixapi.WebHook'),
),
migrations.AlterField(
model_name='webhookeventlistener',
name='webhook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='listeners', to='pretixapi.WebHook'),
),
migrations.AddField(
model_name='webhookcall',
name='action_type',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -68,3 +68,41 @@ class OAuthRefreshToken(AbstractRefreshToken):
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
related_name="refresh_token"
)
class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
target_url = models.URLField(verbose_name=_("Target URL"))
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
@property
def action_types(self):
return [
l.action_type for l in self.listeners.all()
]
class WebHookEventListener(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='listeners')
action_type = models.CharField(max_length=255)
class Meta:
ordering = ("action_type",)
class WebHookCall(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
datetime = models.DateTimeField(auto_now_add=True)
target_url = models.URLField()
action_type = models.CharField(max_length=255)
is_retry = models.BooleanField(default=False)
execution_time = models.FloatField(null=True)
return_code = models.PositiveIntegerField(default=0)
success = models.BooleanField(default=False)
payload = models.TextField()
response_body = models.TextField()
class Meta:
ordering = ("-datetime",)

View File

@@ -19,18 +19,19 @@ class CartPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = CartPosition
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers',)
class CartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers',)
def create(self, validated_data):
@@ -65,6 +66,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
quota.name
)
)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
@@ -118,4 +124,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'You cannot specify a variation for this item.'
)
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
return data

View File

@@ -1,9 +1,11 @@
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_countries.serializers import CountryFieldMixin
from rest_framework.fields import Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event, TaxRule
@@ -46,7 +48,7 @@ class EventSerializer(I18nAwareModelSerializer):
class Meta:
model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from',
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
@@ -94,7 +96,7 @@ class EventSerializer(I18nAwareModelSerializer):
from pretix.base.plugins import get_all_plugins
plugins_available = {
p.module for p in get_all_plugins()
p.module for p in get_all_plugins(self.instance)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
@@ -107,7 +109,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
plugins = validated_data.pop('plugins', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
event = super().create(validated_data)
# Meta data
@@ -121,6 +123,7 @@ class EventSerializer(I18nAwareModelSerializer):
# Plugins
if plugins is not None:
event.set_active_plugins(plugins)
event.save(update_fields=['plugins'])
return event
@@ -190,12 +193,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
event = SlugRelatedField(slug_field='slug', read_only=True)
meta_data = MetaDataField(source='*')
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location',
'presale_start', 'presale_end', 'location', 'event',
'item_price_overrides', 'variation_price_overrides', 'meta_data')

View File

@@ -15,13 +15,20 @@ class I18nField(Field):
super().__init__(**kwargs)
def to_representation(self, value):
if value is None or value.data is None:
if hasattr(value, 'data'):
if isinstance(value.data, dict):
return value.data
elif value.data is None:
return None
else:
return {
settings.LANGUAGE_CODE: str(value.data)
}
elif value is None:
return None
if isinstance(value.data, dict):
return value.data
else:
return {
settings.LANGUAGE_CODE: str(value.data)
settings.LANGUAGE_CODE: str(value)
}
def to_internal_value(self, data):

View File

@@ -74,12 +74,12 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
'variations', 'addons', 'original_price', 'require_approval')
'variations', 'addons', 'original_price', 'require_approval', 'generate_tickets')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):

View File

@@ -11,6 +11,8 @@ from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
@@ -35,11 +37,12 @@ class CompatibleCountryField(serializers.Field):
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'vat_id_validated', 'internal_reference')
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs):
@@ -48,6 +51,15 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
v.required = False
v.allow_blank = True
def validate(self, data):
if data.get('name') and data.get('name_parts'):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
return data
class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
@@ -77,7 +89,8 @@ class CheckinSerializer(I18nAwareModelSerializer):
class OrderDownloadsField(serializers.Field):
def to_representation(self, instance: Order):
if instance.status != Order.STATUS_PAID:
return []
if instance.status != Order.STATUS_PENDING or instance.require_approval or not instance.event.settings.ticket_download_pending:
return []
request = self.context['request']
res = []
@@ -100,10 +113,9 @@ class OrderDownloadsField(serializers.Field):
class PositionDownloadsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
if instance.order.status != Order.STATUS_PAID:
return []
if instance.addon_to_id and not instance.order.event.settings.ticket_download_addons:
return []
if not instance.item.admission and not instance.order.event.settings.ticket_download_nonadm:
if instance.order.status != Order.STATUS_PENDING or instance.order.require_approval or not instance.order.event.settings.ticket_download_pending:
return []
if not instance.generate_ticket:
return []
request = self.context['request']
@@ -129,13 +141,21 @@ class PdfDataSerializer(serializers.Field):
res = {}
ev = instance.subevent or instance.order.event
with language(instance.order.locale):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list.
pdfvars = get_variables(instance.order.event)
for k, f in pdfvars.items():
res[k] = f['evaluate'](instance, instance.order, ev)
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
for k, v in ev.meta_data.items():
res['meta:' + k] = v
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
return res
@@ -149,9 +169,9 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -211,9 +231,9 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval')
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -296,14 +316,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret):
if secret and OrderPosition.objects.filter(order__event=self.context['event'], secret=secret).exists():
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError(
'You cannot assign a position secret that already exists.'
)
@@ -350,6 +371,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'variation': ['You cannot specify a variation for this item.']}
)
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
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
return data
@@ -386,7 +413,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
def validate_payment_provider(self, pp):
@@ -394,6 +421,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
return channel
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(
@@ -455,7 +487,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}')
if 'invoice_address' in validated_data:
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
iadata = validated_data.pop('invoice_address')
name = iadata.pop('name', '')
if name and not iadata.get('name_parts'):
iadata['name_parts'] = {
'_legacy': name
}
ia = InvoiceAddress(**iadata)
else:
ia = None
@@ -507,6 +545,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if any(errs):
raise ValidationError({'positions': errs})
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
@@ -517,7 +557,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
payment_date=now()
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
@@ -544,6 +585,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
addon_to = pos_data.pop('addon_to', None)
attendee_name = pos_data.pop('attendee_name', '')
if attendee_name and not pos_data.get('attendee_name_parts'):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()

View File

@@ -1,7 +1,27 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Voucher
class VoucherListSerializer(serializers.ListSerializer):
def create(self, validated_data):
codes = set()
errs = []
err = False
for voucher_data in validated_data:
if voucher_data['code'] in codes:
err = True
errs.append({'code': ['Duplicate voucher code in request.']})
else:
codes.add(voucher_data['code'])
errs.append({})
if err:
raise ValidationError(errs)
return super().create(validated_data)
class VoucherSerializer(I18nAwareModelSerializer):
class Meta:
model = Voucher
@@ -9,6 +29,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer
def validate(self, data):
data = super().validate(data)

View File

@@ -0,0 +1,71 @@
from django.core.exceptions import ValidationError
from rest_framework import serializers
from pretix.api.models import WebHook
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.models import Event
class EventRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return self.context['organizer'].events.all()
class ActionTypesField(serializers.Field):
def to_representation(self, instance: WebHook):
return instance.action_types
def to_internal_value(self, data):
types = get_all_webhook_events()
for d in data:
if d not in types:
raise ValidationError('Invalid action type "%s".' % d)
return {'action_types': data}
class WebHookSerializer(I18nAwareModelSerializer):
limit_events = EventRelatedField(
slug_field='slug',
queryset=Event.objects.none(),
many=True
)
action_types = ActionTypesField(source='*')
class Meta:
model = WebHook
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
for event in full_data.get('limit_events'):
if self.context['organizer'] != event.organizer:
raise ValidationError('One or more events do not belong to this organizer.')
if full_data.get('limit_events') and full_data.get('all_events'):
raise ValidationError('You can set either limit_events or all_events.')
return data
def create(self, validated_data):
action_types = validated_data.pop('action_types')
inst = super().create(validated_data)
for l in action_types:
inst.listeners.create(action_type=l)
return inst
def update(self, instance, validated_data):
action_types = validated_data.pop('action_types', None)
instance = super().update(instance, validated_data)
if action_types is not None:
current_listeners = set(instance.listeners.values_list('action_type', flat=True))
new_listeners = set(action_types)
for l in current_listeners - new_listeners:
instance.listeners.filter(action_type=l).delete()
for l in new_listeners - current_listeners:
instance.listeners.create(action_type=l)
return instance

21
src/pretix/api/signals.py Normal file
View File

@@ -0,0 +1,21 @@
from datetime import timedelta
from django.dispatch import Signal, receiver
from django.utils.timezone import now
from pretix.api.models import WebHookCall
from pretix.base.signals import periodic_task
register_webhook_events = Signal(
providing_args=[]
)
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instances.
"""
@receiver(periodic_task)
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()

View File

@@ -7,7 +7,8 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
checkin, device, event, item, oauth, order, organizer, user, voucher,
waitinglist, webhooks,
)
router = routers.DefaultRouter()
@@ -15,6 +16,8 @@ router.register(r'organizers', organizer.OrganizerViewSet)
orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
@@ -65,4 +68,9 @@ urlpatterns = [
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
url(r"^device/initialize$", device.InitializeView.as_view(), name="device.initialize"),
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
url(r"^me$", user.MeView.as_view(), name="user.me"),
]

View File

@@ -37,6 +37,9 @@ class ConditionalListView:
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if not hasattr(request, 'event'):
return super().list(request, **kwargs)
lmd = request.event.logentry_set.filter(
content_type__model=self.queryset.model._meta.model_name,
content_type__app_label=self.queryset.model._meta.app_label,

View File

@@ -1,5 +1,5 @@
from django.core.exceptions import ValidationError
from django.db.models import Count, F, Max, OuterRef, Prefetch, Subquery
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
from django.db.models.functions import Coalesce
from django.http import Http404
from django.shortcuts import get_object_or_404
@@ -16,7 +16,9 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
from pretix.base.models import (
Checkin, CheckinList, Event, Order, OrderPosition,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
@@ -154,7 +156,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name', 'positionid')
ordering = ('attendee_name_cached', 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
@@ -162,11 +164,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'-attendee_name': {
'_order': F('display_name').desc(nulls_last=True),
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'last_checked_in': {
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
@@ -201,12 +203,39 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
).prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
).select_related('item', 'variation', 'order', 'addon_to')
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
@@ -244,11 +273,15 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=dt,
questions_supported=self.request.data.get('questions_supported', True)
questions_supported=self.request.data.get('questions_supported', True),
user=self.request.user,
auth=self.request.auth,
)
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -256,9 +289,21 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except CheckInError as e:
return Response({
'status': 'error',
'reason': e.code
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
obj = get_object_or_404(queryset, secret=self.kwargs['pk'])
return obj

View File

@@ -0,0 +1,113 @@
import logging
from django.utils.timezone import now
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.views import APIView
from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.base.models import Device
from pretix.base.models.devices import generate_api_token
logger = logging.getLogger(__name__)
class InitializationRequestSerializer(serializers.Serializer):
token = serializers.CharField(max_length=190)
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
class UpdateRequestSerializer(serializers.Serializer):
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
class DeviceSerializer(serializers.ModelSerializer):
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
class Meta:
model = Device
fields = [
'organizer', 'device_id', 'unique_serial', 'api_token',
'name'
]
class InitializeView(APIView):
authentication_classes = tuple()
permission_classes = tuple()
def post(self, request, format=None):
serializer = InitializationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
device = Device.objects.get(initialization_token=serializer.validated_data.get('token'))
except Device.DoesNotExist:
raise ValidationError({'token': ['Unknown initialization token.']})
if device.initialized:
raise ValidationError({'token': ['This initialization token has already been used.']})
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.api_token = generate_api_token()
device.save()
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
serializer = DeviceSerializer(device)
return Response(serializer.data)
class UpdateView(APIView):
authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None):
serializer = UpdateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
device = request.auth
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
serializer = DeviceSerializer(device)
return Response(serializer.data)
class RollKeyView(APIView):
authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None):
device = request.auth
device.api_token = generate_api_token()
device.save()
device.log_action('pretix.device.keyroll', auth=device)
serializer = DeviceSerializer(device)
return Response(serializer.data)
class RevokeKeyView(APIView):
authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None):
device = request.auth
device.api_token = None
device.save()
device.log_action('pretix.device.revoked', auth=device)
serializer = DeviceSerializer(device)
return Response(serializer.data)

View File

@@ -1,5 +1,7 @@
import django_filters
from django.db import transaction
from django.db.models import ProtectedError
from django.db.models import ProtectedError, Q
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
@@ -10,20 +12,79 @@ from pretix.api.serializers.event import (
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import Event, ItemCategory, TaxRule
from pretix.base.models import (
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents']
def ends_after_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
queryset = Event.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'event'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = EventFilter
def get_queryset(self):
return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property')
if isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = self.request.auth.get_events_with_any_permission()
elif self.request.user.is_authenticated:
qs = self.request.user.get_events_with_any_permission(self.request).filter(
organizer=self.request.organizer
)
return qs.prefetch_related(
'meta_values', 'meta_values__property'
)
def perform_update(self, serializer):
current_live_value = serializer.instance.live
@@ -120,9 +181,40 @@ class CloneEventViewSet(viewsets.ModelViewSet):
class SubEventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = SubEvent
fields = ['active']
fields = ['active', 'event__live']
def ends_after_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
@@ -132,7 +224,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
filterset_class = SubEventFilter
def get_queryset(self):
return self.request.event.subevents.prefetch_related(
if getattr(self.request, 'event', None):
qs = self.request.event.subevents
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.auth.get_events_with_any_permission()
)
elif self.request.user.is_authenticated:
qs = SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_any_permission()
)
return qs.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set'
)

View File

@@ -42,7 +42,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filterset_class = ItemFilter
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -83,6 +83,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
user=self.request.user,
auth=self.request.auth,
)
self.get_object().cartposition_set.all().delete()
super().perform_destroy(instance)
@@ -92,7 +93,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -154,7 +155,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -210,7 +211,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = ItemCategoryFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -264,7 +265,8 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = QuestionFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
@@ -307,7 +309,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -362,7 +364,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = QuotaFilter
ordering_fields = ('id', 'size')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):

View File

@@ -1,10 +1,11 @@
import datetime
from decimal import Decimal
import django_filters
import pytz
from django.db import transaction
from django.db.models import Q
from django.db.models.functions import Concat
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
@@ -15,7 +16,7 @@ from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
@@ -25,8 +26,8 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
@@ -38,9 +39,7 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.services.tickets import generate
from pretix.base.signals import order_placed, register_ticket_outputs
@@ -52,15 +51,15 @@ class OrderFilter(FilterSet):
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'require_approval']
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status')
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
@@ -72,13 +71,35 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return ctx
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'positions__answers__question', 'fees', 'payments', 'refunds', 'refunds__payment'
qs = self.request.event.orders.prefetch_related(
'fees', 'payments', 'refunds', 'refunds__payment'
).select_related(
'invoice_address'
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
)
)
)
else:
qs = qs.prefetch_related(
Prefetch(
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
@@ -109,9 +130,11 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
ct = get_cachedticket_for_order(order, provider.identifier)
if not ct.file:
ct = CachedCombinedTicket.objects.filter(
order=order, provider=provider.identifier, file__isnull=False
).last()
if not ct or not ct.file:
generate.apply_async(args=('order', order.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
@@ -165,6 +188,12 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
@detail_route(methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
cancellation_fee = request.data.get('cancellation_fee', None)
if cancellation_fee:
try:
cancellation_fee = float(Decimal(cancellation_fee))
except:
cancellation_fee = None
order = self.get_object()
if not order.cancel_allowed():
@@ -173,13 +202,21 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail
)
try:
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
device=request.auth if isinstance(request.auth, Device) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail,
cancellation_fee=cancellation_fee
)
except OrderError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@@ -191,7 +228,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
approve_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
)
except Quota.QuotaExceededException as e:
@@ -210,7 +247,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
deny_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
comment=comment,
)
@@ -229,7 +266,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
)
order.status = Order.STATUS_PENDING
order.save()
order.save(update_fields=['status'])
order.log_action(
'pretix.event.order.unpaid',
user=request.user if request.user.is_authenticated else None,
@@ -267,7 +304,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_order_refunded(
order,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
)
return self.retrieve(request, [], **kwargs)
@@ -341,6 +378,13 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def perform_create(self, serializer):
serializer.save()
def perform_destroy(self, instance):
if not instance.testmode:
raise PermissionDenied('Only test mode orders can be deleted.')
with transaction.atomic():
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
@@ -351,17 +395,18 @@ class OrderPositionFilter(FilterSet):
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name__icontains=value)
| Q(addon_to__attendee_name__icontains=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name__icontains=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name__iexact=value) | Q(addon_to__attendee_name__iexact=value))
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
class Meta:
model = OrderPosition
@@ -387,13 +432,45 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
filterset_class = OrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'-attendee_name': {
'_order': F('display_name').asc(nulls_last=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
}
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
qs = OrderPosition.objects.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
else:
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
@@ -410,14 +487,14 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if pos.order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
if pos.addon_to_id and not request.event.settings.ticket_download_addons:
raise PermissionDenied("Downloads are not enabled for add-on products.")
if not pos.item.admission and not request.event.settings.ticket_download_nonadm:
raise PermissionDenied("Downloads are not enabled for non-admission products.")
if not pos.generate_ticket:
raise PermissionDenied("Downloads are not enabled for this product.")
ct = get_cachedticket_for_position(pos, provider.identifier)
if not ct.file:
ct = CachedTicket.objects.filter(
order_position=pos, provider=provider.identifier, file__isnull=False
).last()
if not ct or not ct.file:
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
@@ -481,7 +558,10 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('amount', str(payment.amount))
)
mark_refunded = request.data.get('mark_refunded', False)
if 'mark_refunded' in request.data:
mark_refunded = request.data.get('mark_refunded', False)
else:
mark_refunded = request.data.get('mark_canceled', False)
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
@@ -534,7 +614,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
payment.order.event.subevents.filter(
id__in=payment.order.positions.values_list('subevent_id', flat=True))
)
payment.order.save()
payment.order.save(update_fields=['status', 'expires'])
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
@detail_route(methods=['POST'])
@@ -590,17 +670,21 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
if request.data.get('mark_refunded', False):
if 'mark_refunded' in request.data:
mark_refunded = request.data.get('mark_refunded', False)
else:
mark_refunded = request.data.get('mark_canceled', False)
if mark_refunded:
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth)
else:
elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0):
refund.order.status = Order.STATUS_PENDING
refund.order.set_expires(
now(),
refund.order.event.subevents.filter(
id__in=refund.order.positions.values_list('subevent_id', flat=True))
)
refund.order.save()
refund.order.save(update_fields=['status', 'expires'])
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@@ -619,7 +703,10 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return ctx
def create(self, request, *args, **kwargs):
mark_refunded = request.data.pop('mark_refunded', False)
if 'mark_refunded' in request.data:
mark_refunded = request.data.pop('mark_refunded', False)
else:
mark_refunded = request.data.pop('mark_canceled', False)
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
@@ -736,7 +823,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
raise PermissionDenied('The invoice file is no longer stored on the server.')
else:
c = generate_cancellation(inv)
if inv.order.status not in (Order.STATUS_CANCELED, Order.STATUS_REFUNDED):
if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(inv.order)
else:
inv = c

View File

@@ -23,5 +23,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
)
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
elif hasattr(self.request.auth, 'organizer_id'):
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
else:
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)

View File

@@ -0,0 +1,16 @@
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
class MeView(APIView):
authentication_classes = (SessionAuthentication, OAuth2Authentication)
def get(self, request, format=None):
return Response({
'email': request.user.email,
'fullname': request.user.fullname,
'locale': request.user.locale,
'timezone': request.user.timezone
})

View File

@@ -1,11 +1,16 @@
import contextlib
from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from rest_framework import viewsets
from rest_framework import status, viewsets
from rest_framework.decorators import list_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
@@ -41,8 +46,29 @@ class VoucherViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.vouchers.all()
def _predict_quota_check(self, data, instance):
# This method predicts if Voucher.clean_quota_needs_checking
# *migh* later require a quota check. It is only approximate
# and returns True a little too often. The point is to avoid
# locks when we know we won't need them.
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
return False
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
return False
if 'block_quota' in data and not data.get('block_quota'):
return False
if instance and 'block_quota' not in data and not instance.block_quota:
return False
return True
def create(self, request, *args, **kwargs):
with request.event.lock():
if self._predict_quota_check(request.data, None):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
@@ -60,7 +86,11 @@ class VoucherViewSet(viewsets.ModelViewSet):
return ctx
def update(self, request, *args, **kwargs):
with request.event.lock():
if self._predict_quota_check(request.data, self.get_object()):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
@@ -82,3 +112,24 @@ class VoucherViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
super().perform_destroy(instance)
@list_route(methods=['POST'])
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i, v in enumerate(serializer.instance):
v.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data[i]
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

View File

@@ -0,0 +1,49 @@
from rest_framework import viewsets
from pretix.api.models import WebHook
from pretix.api.serializers.webhooks import WebHookSerializer
from pretix.helpers.dicts import merge_dicts
class WebHookViewSet(viewsets.ModelViewSet):
serializer_class = WebHookSerializer
queryset = WebHook.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
def get_queryset(self):
return self.request.organizer.webhooks.prefetch_related('listeners')
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.webhook.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
def perform_update(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.webhook.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
return inst
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.webhook.changed',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk, 'enabled': False}
)
instance.enabled = False
instance.save(update_fields=['enabled'])

253
src/pretix/api/webhooks.py Normal file
View File

@@ -0,0 +1,253 @@
import json
import logging
import time
from collections import OrderedDict
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 requests import RequestException
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
from pretix.api.signals import register_webhook_events
from pretix.base.models import LogEntry
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.celery_app import app
logger = logging.getLogger(__name__)
_ALL_EVENTS = None
class WebhookEvent:
def __init__(self):
pass
def __repr__(self):
return '<WebhookEvent: {}>'.format(self.action_type)
@property
def action_type(self) -> str:
"""
The action_type string that this notification handles, for example
``"pretix.event.order.paid"``. Only one notification type should be registered
per action type.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this notification type.
"""
raise NotImplementedError() # NOQA
def build_payload(self, logentry: LogEntry) -> dict:
"""
This is the main function that you should override. It is supposed to turn a log entry
object into a dictionary that can be used as the webhook payload.
"""
raise NotImplementedError() # NOQA
def get_all_webhook_events():
global _ALL_EVENTS
if _ALL_EVENTS:
return _ALL_EVENTS
types = OrderedDict()
for recv, ret in register_webhook_events.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.action_type] = r
else:
types[ret.action_type] = ret
_ALL_EVENTS = types
return types
class ParametrizedOrderWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
order = logentry.content_object
return {
'notification_id': logentry.pk,
'organizer': order.event.organizer.slug,
'event': order.event.slug,
'code': order.code,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
return d
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
ParametrizedOrderWebhookEvent(
'pretix.event.order.placed',
_('New order placed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.placed.require_approval',
_('New order requires approval'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.paid',
_('Order marked as paid'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.canceled',
_('Order canceled'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.expired',
_('Order expired'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.modified',
_('Order information changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.contact.changed',
_('Order contact address changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.changed.*',
_('Order changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.refund.created.externally',
_('External refund of payment'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.approved',
_('Order approved'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.denied',
_('Order denied'),
),
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
),
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin.reverted',
_('Ticket check-in reverted'),
),
)
@app.task(base=TransactionAwareTask)
def notify_webhooks(logentry_id: int):
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.organizer:
return # We need to know the organizer
types = get_all_webhook_events()
notification_type = None
typepath = logentry.action_type
while not notification_type and '.' in typepath:
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if not notification_type:
return # Ignore, no webhooks for this event type
# All webhooks that registered for this notification
event_listener = WebHookEventListener.objects.filter(
webhook=OuterRef('pk'),
action_type=notification_type.action_type
)
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
organizer=logentry.organizer,
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
for wh in webhooks:
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
logentry = LogEntry.all.get(id=logentry_id)
webhook = WebHook.objects.get(id=webhook_id)
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
payload = event_type.build_payload(logentry)
t = time.time()
try:
try:
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError:
pass

View File

@@ -0,0 +1,66 @@
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import register_sales_channels
logger = logging.getLogger(__name__)
_ALL_CHANNELS = None
class SalesChannel:
def __repr__(self):
return '<SalesChannel: {}>'.format(self.identifier)
@property
def identifier(self) -> str:
"""
The internal identifier of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def icon(self) -> str:
"""
The name of a Font Awesome icon to represent this channel
"""
return "circle"
def get_all_sales_channels():
global _ALL_CHANNELS
if _ALL_CHANNELS:
return _ALL_CHANNELS
types = OrderedDict()
for recv, ret in register_sales_channels.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_CHANNELS = types
return types
class WebshopSalesChannel(SalesChannel):
identifier = "web"
verbose_name = _('Online shop')
icon = "globe"
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
def base_sales_channels(sender, **kwargs):
return (
WebshopSalesChannel(),
)

View File

@@ -1,5 +1,5 @@
import logging
from smtplib import SMTPRecipientsRefused, SMTPSenderRefused
from smtplib import SMTPResponseException
import bleach
import markdown
@@ -23,16 +23,14 @@ class CustomSMTPBackend(EmailBackend):
try:
self.open()
self.connection.ehlo_or_helo_if_needed()
self.connection.rcpt("test@example.org")
(code, resp) = self.connection.mail(from_addr, [])
if code != 250:
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPSenderRefused(code, resp, from_addr)
senderrs = {}
(code, resp) = self.connection.rcpt('test@example.com')
raise SMTPResponseException(code, resp)
(code, resp) = self.connection.rcpt('testdummy@pretix.eu')
if (code != 250) and (code != 251):
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPRecipientsRefused(senderrs)
raise SMTPResponseException(code, resp)
finally:
self.close()
@@ -97,7 +95,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
@property
def template_name(self):
raise NotImplemented
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
body_md = bleach.linkify(markdown_compile(plain_body))

View File

@@ -1,5 +1,14 @@
import io
import tempfile
from collections import OrderedDict
from typing import Tuple
from defusedcsv import csv
from django import forms
from django.utils.translation import ugettext, ugettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
class BaseExporter:
"""
@@ -55,7 +64,7 @@ class BaseExporter:
"""
return {}
def render(self, form_data: dict) -> Tuple[str, str, str]:
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
"""
Render the exported file and return a tuple consisting of a filename, a file type
and file content.
@@ -69,3 +78,138 @@ class BaseExporter:
tasks.
"""
raise NotImplementedError() # NOQA
class ListExporter(BaseExporter):
@property
def export_form_fields(self) -> dict:
ff = OrderedDict(
[
('_format',
forms.ChoiceField(
label=_('Export format'),
choices=(
('xlsx', _('Excel (.xlsx)')),
('default', _('CSV (with commas)')),
('excel', _('CSV (Excel-style)')),
('semicolon', _('CSV (with semicolons)')),
),
)),
]
)
ff.update(self.additional_form_fields)
return ff
@property
def additional_form_fields(self) -> dict:
return {}
def iterate_list(self, form_data):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export.csv'
def _render_csv(self, form_data, **kwargs):
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_list(form_data):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data):
wb = Workbook()
ws = wb.get_active_sheet()
try:
ws.title = str(self.verbose_name)
except:
pass
for i, line in enumerate(self.iterate_list(form_data)):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
if form_data.get('_format') == 'xlsx':
return self._render_xlsx(form_data)
elif form_data.get('_format') == 'default':
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
elif form_data.get('_format') == 'csv-excel':
return self._render_csv(form_data, dialect='excel')
elif form_data.get('_format') == 'semicolon':
return self._render_csv(form_data, dialect='excel', delimiter=';')
class MultiSheetListExporter(ListExporter):
@property
def sheets(self):
raise NotImplementedError()
@property
def export_form_fields(self) -> dict:
choices = [
('xlsx', _('Combined Excel (.xlsx)')),
]
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)')),
]
ff = OrderedDict(
[
('_format',
forms.ChoiceField(
label=_('Export format'),
choices=choices,
)),
]
)
ff.update(self.additional_form_fields)
return ff
def iterate_list(self, form_data):
pass
def iterate_sheet(self, form_data, sheet):
raise NotImplementedError() # noqa
def _render_sheet_csv(self, form_data, sheet, **kwargs):
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data):
wb = Workbook()
ws = wb.get_active_sheet()
wb.remove(ws)
for s, l in self.sheets:
ws = wb.create_sheet(str(l))
for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
if form_data.get('_format') == 'xlsx':
return self._render_xlsx(form_data)
elif ':' in form_data.get('_format'):
sheet, f = form_data.get('_format').split(':')
if f == 'default':
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
elif f == 'excel':
return self._render_sheet_csv(form_data, sheet, dialect='excel')
elif f == 'semicolon':
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';')

View File

@@ -27,7 +27,7 @@ class InvoiceExporter(BaseExporter):
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('pk')) & Q(provider=form_data.get('payment_provider'))
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
)
)
)

View File

@@ -10,7 +10,7 @@ from ..signals import register_data_exporters
class JSONExporter(BaseExporter):
identifier = 'json'
verbose_name = 'JSON'
verbose_name = 'Order data (JSON)'
def render(self, form_data):
jo = {

View File

@@ -1,28 +1,37 @@
import io
from collections import OrderedDict
from decimal import Decimal
import pytz
from defusedcsv import csv
from django import forms
from django.db.models import DateTimeField, Max, OuterRef, Subquery, Sum
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
from django.dispatch import receiver
from django.utils.formats import localize
from django.utils.formats import date_format, localize
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import BaseExporter
from ..exporter import ListExporter, MultiSheetListExporter
from ..signals import register_data_exporters
class OrderListExporter(BaseExporter):
identifier = 'orderlistcsv'
verbose_name = ugettext_lazy('List of orders (CSV)')
class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist'
verbose_name = ugettext_lazy('Order data')
@property
def export_form_fields(self):
def sheets(self):
return (
('orders', _('Orders')),
('positions', _('Order positions')),
('fees', _('Order fees')),
)
@property
def additional_form_fields(self):
return OrderedDict(
[
('paid_only',
@@ -50,10 +59,16 @@ class OrderListExporter(BaseExporter):
tax_rates = sorted(tax_rates)
return tax_rates
def render(self, form_data: dict):
output = io.StringIO()
def iterate_sheet(self, form_data, sheet):
if sheet == 'orders':
return self.iterate_orders(form_data)
elif sheet == 'positions':
return self.iterate_positions(form_data)
elif sheet == 'fees':
return self.iterate_fees(form_data)
def iterate_orders(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
@@ -74,7 +89,14 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -87,7 +109,7 @@ class OrderListExporter(BaseExporter):
headers.append(_('Invoice numbers'))
writer.writerow(headers)
yield headers
full_fee_sum_cache = {
o['order__id']: o['grosssum'] for o in
@@ -118,6 +140,13 @@ class OrderListExporter(BaseExporter):
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
@@ -126,7 +155,7 @@ class OrderListExporter(BaseExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += ['', '', '', '', '', '', '']
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -147,17 +176,193 @@ class OrderListExporter(BaseExporter):
]
row.append(', '.join([i.number for i in order.invoices.all()]))
writer.writerow(row)
yield row
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def iterate_fees(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderFee.objects.filter(
order__event=self.event,
).select_related('order', 'order__invoice_address', 'tax_rule')
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Order code'),
_('Status'),
_('Email'),
_('Order date'),
_('Fee type'),
_('Description'),
_('Price'),
_('Tax rate'),
_('Tax rule'),
_('Tax value'),
_('Company'),
_('Invoice address name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
]
yield headers
for op in qs.order_by('order__datetime'):
order = op.order
row = [
order.code,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
op.get_fee_type_display(),
op.description,
op.value,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
op.tax_value,
]
try:
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderPosition.objects.filter(
order__event=self.event,
).select_related(
'order', 'order__invoice_address', 'item', 'variation',
'voucher', 'tax_rule'
).prefetch_related(
'answers', 'answers__question'
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Order code'),
_('Position ID'),
_('Status'),
_('Email'),
_('Order date'),
_('Product'),
_('Variation'),
_('Price'),
_('Tax rate'),
_('Tax rule'),
_('Tax value'),
_('Attendee name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
_('Attendee email'),
_('Voucher'),
_('Pseudonymization ID'),
]
questions = list(self.event.questions.all())
for q in questions:
headers.append(str(q.question))
headers += [
_('Company'),
_('Invoice address name'),
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
]
yield headers
for op in qs.order_by('order__datetime', 'positionid'):
order = op.order
row = [
order.code,
op.positionid,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
str(op.item),
str(op.variation) if op.variation else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
op.tax_value,
op.attendee_name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
op.attendee_name_parts.get(k, '')
)
row += [
op.attendee_email,
op.voucher.code if op.voucher else '',
op.pseudonymization_id,
]
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)
for q in questions:
row.append(acache.get(q.pk, ''))
try:
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def get_filename(self):
return '{}_orders'.format(self.event.slug)
class PaymentListExporter(BaseExporter):
identifier = 'paymentlistcsv'
verbose_name = ugettext_lazy('List of payments and refunds (CSV)')
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = ugettext_lazy('Order payments and refunds')
@property
def export_form_fields(self):
def additional_form_fields(self):
return OrderedDict(
[
('successful_only',
@@ -169,10 +374,8 @@ class PaymentListExporter(BaseExporter):
]
)
def render(self, form_data: dict):
output = io.StringIO()
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
provider_names = {
k: v.verbose_name
@@ -198,9 +401,9 @@ class PaymentListExporter(BaseExporter):
headers = [
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Amount'), _('Payment method')
_('Status code'), _('Amount'), _('Payment method')
]
writer.writerow(headers)
yield headers
for obj in objs:
if isinstance(obj, OrderPayment) and obj.payment_date:
@@ -215,27 +418,26 @@ class PaymentListExporter(BaseExporter):
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
d2,
obj.get_state_display(),
obj.state,
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
provider_names.get(obj.provider, obj.provider)
]
writer.writerow(row)
yield row
return '{}_payments.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def get_filename(self):
return '{}_payments'.format(self.event.slug)
class QuotaListExporter(BaseExporter):
identifier = 'quotalistcsv'
verbose_name = ugettext_lazy('Quota availabilities (CSV)')
def render(self, form_data: dict):
output = io.StringIO()
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
class QuotaListExporter(ListExporter):
identifier = 'quotalist'
verbose_name = ugettext_lazy('Quota availabilities')
def iterate_list(self, form_data):
headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Current availability')
]
writer.writerow(headers)
yield headers
for quota in self.event.quotas.all():
avail = quota.availability()
@@ -249,9 +451,180 @@ class QuotaListExporter(BaseExporter):
quota.count_waiting_list_pending(),
_('Infinite') if avail[1] is None else avail[1]
]
writer.writerow(row)
yield row
return '{}_quotas.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
def get_filename(self):
return '{}_quotas'.format(self.event.slug)
class InvoiceDataExporter(MultiSheetListExporter):
identifier = 'invoicedata'
verbose_name = ugettext_lazy('Invoice data')
@property
def sheets(self):
return (
('invoices', _('Invoices')),
('lines', _('Invoice lines')),
)
def iterate_sheet(self, form_data, sheet):
if sheet == 'invoices':
yield [
_('Invoice number'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Language'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Reverse charge'),
_('Shown foreign currency'),
_('Foreign currency rate'),
_('Total value (with taxes)'),
_('Total value (without taxes)'),
]
qs = self.event.invoices.order_by('full_invoice_no').select_related(
'order', 'refers'
).annotate(
total_gross=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum('gross_value')
).values('s')
),
total_net=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum(F('gross_value') - F('tax_value'))
).values('s')
)
)
for i in qs:
yield [
i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.locale,
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
_('Yes') if i.reverse_charge else _('No'),
i.foreign_currency_display,
i.foreign_currency_rate,
i.total_gross if i.total_gross else Decimal('0.00'),
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
]
elif sheet == 'lines':
yield [
_('Invoice number'),
_('Line number'),
_('Description'),
_('Gross price'),
_('Net price'),
_('Tax value'),
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
]
qs = InvoiceLine.objects.filter(
invoice__event=self.event
).order_by('invoice__full_invoice_no', 'position').select_related(
'invoice', 'invoice__order', 'invoice__refers'
)
for l in qs:
i = l.invoice
yield [
i.full_invoice_no,
l.position + 1,
l.description.replace("<br />", " - "),
l.gross_value,
l.net_value,
l.tax_value,
l.tax_rate,
l.tax_name,
date_format(l.event_date_from, "SHORT_DATE_FORMAT") if l.event_date_from else "",
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
]
def get_filename(self):
return '{}_invoices'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
@@ -267,3 +640,8 @@ def register_paymentlist_exporter(sender, **kwargs):
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
def register_quotalist_exporter(sender, **kwargs):
return QuotaListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter

View File

@@ -1,9 +1,11 @@
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
from pretix.base.models import Event
@@ -57,7 +59,7 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
kwargs['locales'] = self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
for f in self.fields.values():
for k, f in self.fields.items():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
@@ -71,3 +73,29 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
# TODO: make sure pub is always correct
return 'pub/' + fname
class PrefixForm(forms.Form):
prefix = forms.CharField(widget=forms.HiddenInput)
class SafeSessionWizardView(SessionWizardView):
def get_prefix(self, request, *args, **kwargs):
if hasattr(request, '_session_wizard_prefix'):
return request._session_wizard_prefix
prefix_form = PrefixForm(self.request.POST, prefix=super().get_prefix(request, *args, **kwargs))
if not prefix_form.is_valid():
request._session_wizard_prefix = get_random_string(length=24)
else:
request._session_wizard_prefix = prefix_form.cleaned_data['prefix']
return request._session_wizard_prefix
def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
context['wizard']['prefix_form'] = PrefixForm(
prefix=super().get_prefix(self.request),
initial={
'prefix': self.get_prefix(self.request)
}
)
return context

View File

@@ -120,7 +120,7 @@ class RegistrationForm(forms.Form):
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
if User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError(
self.error_messages['duplicate_email'],
code='duplicate_email'

View File

@@ -1,3 +1,4 @@
import copy
import logging
from decimal import Decimal
@@ -8,6 +9,7 @@ import vat_moss.id
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.widgets import (
@@ -16,12 +18,112 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
class NamePartsWidget(forms.MultiWidget):
widget = forms.TextInput
def __init__(self, scheme: dict, field: forms.Field, attrs=None):
widgets = []
self.scheme = scheme
self.field = field
for fname, label, size in self.scheme['fields']:
a = copy.copy(attrs) or {}
a['data-fname'] = fname
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
def decompress(self, value):
if value is None:
return None
data = []
for i, field in enumerate(self.scheme['fields']):
fname, label, size = field
data.append(value.get(fname, ""))
if '_legacy' in value and not data[-1]:
data[-1] = value.get('_legacy', '')
return data
def render(self, name: str, value, attrs=None, renderer=None) -> str:
if not isinstance(value, list):
value = self.decompress(value)
output = []
final_attrs = self.build_attrs(attrs or dict())
if 'required' in final_attrs:
del final_attrs['required']
id_ = final_attrs.get('id', None)
for i, widget in enumerate(self.widgets):
try:
widget_value = value[i]
except (IndexError, TypeError):
widget_value = None
if id_:
final_attrs = dict(
final_attrs,
id='%s_%s' % (id_, i),
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
final_attrs['data-size'] = self.scheme['fields'][i][2]
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets) -> str:
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
class NamePartsFormField(forms.MultiValueField):
widget = NamePartsWidget
def compress(self, data_list) -> dict:
data = {}
data['_scheme'] = self.scheme_name
for i, value in enumerate(data_list):
data[self.scheme['fields'][i][0]] = value or ''
return data
def __init__(self, *args, **kwargs):
fields = []
defaults = {
'widget': self.widget,
'max_length': kwargs.pop('max_length', None),
}
self.scheme_name = kwargs.pop('scheme')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
self.one_required = kwargs.get('required', True)
require_all_fields = kwargs.pop('require_all_fields', False)
kwargs['required'] = False
kwargs['widget'] = (kwargs.get('widget') or self.widget)(
scheme=self.scheme, field=self, **kwargs.pop('widget_kwargs', {})
)
defaults.update(**kwargs)
for fname, label, size in self.scheme['fields']:
defaults['label'] = label
field = forms.CharField(**defaults)
field.part_name = fname
fields.append(field)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
self.require_all_fields = require_all_fields
self.required = self.one_required
def clean(self, value) -> dict:
value = super().clean(value)
if self.one_required and (not value or not any(v for v in value)):
raise forms.ValidationError(self.error_messages['required'], code='required')
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
return value
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -46,10 +148,12 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs)
if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name'] = forms.CharField(
max_length=255, required=event.settings.attendee_names_required,
self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required,
scheme=event.settings.name_scheme,
label=_('Attendee name'),
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
@@ -66,6 +170,7 @@ class BaseQuestionsForm(forms.Form):
else:
initial = None
tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text)
if q.type == Question.TYPE_BOOLEAN:
if q.required:
# For some reason, django-bootstrap3 does not set the required attribute
@@ -81,7 +186,7 @@ class BaseQuestionsForm(forms.Form):
field = forms.BooleanField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=initialbool, widget=widget,
)
elif q.type == Question.TYPE_NUMBER:
@@ -94,13 +199,13 @@ class BaseQuestionsForm(forms.Form):
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_TEXT:
field = forms.CharField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
widget=forms.Textarea,
initial=initial.answer if initial else None,
)
@@ -108,7 +213,7 @@ class BaseQuestionsForm(forms.Form):
field = forms.ModelChoiceField(
queryset=q.options,
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.options.first() if initial else None,
@@ -117,35 +222,35 @@ class BaseQuestionsForm(forms.Form):
field = forms.ModelMultipleChoiceField(
queryset=q.options,
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
widget=forms.CheckboxSelectMultiple,
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
field = forms.FileField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
widget=DatePickerWidget(),
)
elif q.type == Question.TYPE_TIME:
field = forms.TimeField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
field = forms.SplitDateTimeField(
field = SplitDateTimeField(
label=q.question, required=q.required,
help_text=q.help_text,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
@@ -169,13 +274,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference')
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'name': forms.TextInput(attrs={}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -187,19 +292,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.event = event = kwargs.pop('event')
self.request = kwargs.pop('request', None)
self.validate_vat_id = kwargs.pop('validate_vat_id')
self.all_optional = kwargs.pop('all_optional', False)
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
if not event.settings.invoice_address_required:
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
f.widget.is_required = False
if 'required' in f.widget.attrs:
del f.widget.attrs['required']
if event.settings.invoice_name_required:
self.fields['name'].required = True
elif event.settings.invoice_address_company_required:
elif event.settings.invoice_address_company_required and not self.all_optional:
self.initial['is_business'] = True
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
@@ -209,18 +313,37 @@ class BaseInvoiceAddressForm(forms.ModelForm):
del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency']
else:
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.invoice_name_required and not self.all_optional,
scheme=event.settings.name_scheme,
label=_('Name'),
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
def clean(self):
data = self.cleaned_data
if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required:
raise ValidationError(_('You need to provide either a company name or your name.'))
if not data.get('is_business'):
data['company'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
if not data.get('is_business') and not data.get('name_parts'):
raise ValidationError(_('You need to provide your name.'))
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
self.instance.name_parts = data.get('name_parts')
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
@@ -232,7 +355,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
country_code, normalized_id, company_name = result
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except vat_moss.errors.InvalidError:
except (vat_moss.errors.InvalidError, ValueError):
raise ValidationError(_('This VAT ID is not valid. Please re-check your input.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))

View File

@@ -69,7 +69,7 @@ class UserSettingsForm(forms.ModelForm):
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(Q(email=email) & ~Q(pk=self.instance.pk)).exists():
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.instance.pk)).exists():
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',

View File

@@ -2,6 +2,7 @@ import os
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 _
@@ -89,17 +90,25 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control splitdatetimepart')
time_attrs.setdefault('class', 'form-control splitdatetimepart')
date_attrs.setdefault('autocomplete', 'off')
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
def date_placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
def time_placeholder():
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
date_attrs['placeholder'] = lazy(date_placeholder, str)
time_attrs['placeholder'] = lazy(time_placeholder, str)
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),

View File

@@ -4,11 +4,12 @@ from decimal import Decimal
from io import BytesIO
from typing import Tuple
import bleach
import vat_moss.exchange_rates
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext
from django.utils.translation import pgettext, ugettext
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
@@ -31,6 +32,31 @@ from pretix.base.templatetags.money import money_filter
logger = logging.getLogger(__name__)
class NumberedCanvas(Canvas):
def __init__(self, *args, **kwargs):
self.font_regular = kwargs.pop('font_regular')
super().__init__(*args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(num_pages)
Canvas.showPage(self)
Canvas.save(self)
def draw_page_number(self, page_count):
self.saveState()
self.setFont(self.font_regular, 8)
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
self.restoreState()
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.
@@ -79,6 +105,9 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
top_margin = 20 * mm
bottom_margin = 15 * mm
doc_template_class = BaseDocTemplate
canvas_class = Canvas
font_regular = 'OpenSans'
font_bold = 'OpenSansBd'
def _init(self):
"""
@@ -92,10 +121,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
Get a stylesheet. By default, this contains the "Normal" and "Heading1" styles.
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName='OpenSansBd', fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName='OpenSans', fontSize=8, leading=10))
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
return stylesheet
def _register_fonts(self):
@@ -171,7 +200,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
)
])
story = self._get_story(doc)
doc.build(story)
doc.build(story, canvasmaker=self.canvas_class)
return doc
def generate(self, invoice: Invoice):
@@ -192,17 +221,27 @@ class ThumbnailingImageReader(ImageReader):
size=(int(width * dpi / 72), int(height * dpi / 72)),
resample=BICUBIC
)
self._data = None
return width, height
def _jpeg_fh(self):
# Bypass a reportlab-internal optimization that falls back to the original
# file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image.
return None
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
def canvas_class(self, *args, **kwargs):
kwargs['font_regular'] = self.font_regular
return NumberedCanvas(*args, **kwargs)
def _on_other_page(self, canvas: Canvas, doc):
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
@@ -216,7 +255,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
def _draw_invoice_from(self, canvas):
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
@@ -226,72 +265,78 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
canvas.setFont(self.font_regular, 8)
if self.invoice.order.testmode:
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.restoreState()
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
self._draw_invoice_from(canvas)
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
self._draw_invoice_to(canvas)
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
@@ -323,7 +368,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return txt
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
@@ -342,7 +387,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Event').upper())
canvas.drawText(textobject)
@@ -379,7 +424,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Your reference: {reference}').format(reference=self.invoice.internal_reference),
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
bleach.clean(self.invoice.invoice_to_beneficiary, tags=[]).replace("\n", "<br />\n"),
self.stylesheet['Normal']
))
@@ -393,8 +445,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('FONTNAME', (0, 0), (-1, 0), self.font_bold),
('FONTNAME', (0, -1), (-1, -1), self.font_bold),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
]
@@ -462,7 +514,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('FONTSIZE', (0, 0), (-1, -1), 8),
('FONTNAME', (0, 0), (-1, -1), 'OpenSans'),
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
pgettext('invoice', 'Tax rate'),

View File

@@ -129,13 +129,22 @@ def get_language_from_request(request: HttpRequest) -> str:
if _supported is None:
_supported = OrderedDict(settings.LANGUAGES)
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
if request.path.startswith(get_script_prefix() + 'control'):
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
else:
return (
get_language_from_session_or_cookie(request)
or get_language_from_user_settings(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
def _parse_csp(header):
@@ -189,6 +198,7 @@ class SecurityMiddleware(MiddlewareMixin):
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict

View File

@@ -28,7 +28,8 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
('email', models.EmailField(max_length=254, blank=True, unique=True, verbose_name='E-mail', null=True, db_index=True)),
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
db_index=True)),
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
('is_active', models.BooleanField(verbose_name='Is active', default=True)),

View File

@@ -0,0 +1,45 @@
# Generated by Django 2.1 on 2018-09-12 10:35
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.devices
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0098_auto_20180731_1243_squashed_0100_item_require_approval'),
]
operations = [
migrations.CreateModel(
name='Device',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('device_id', models.PositiveIntegerField()),
('unique_serial', models.CharField(default=pretix.base.models.devices.generate_serial, max_length=190, unique=True)),
('initialization_token', models.CharField(default=pretix.base.models.devices.generate_initialization_token, max_length=190, unique=True)),
('api_token', models.CharField(max_length=190, null=True, unique=True)),
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
('name', models.CharField(max_length=190, verbose_name='Name')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Setup date')),
('initialized', models.DateTimeField(null=True, verbose_name='Initialization date')),
('hardware_brand', models.CharField(blank=True, max_length=190, null=True)),
('hardware_model', models.CharField(blank=True, max_length=190, null=True)),
('software_brand', models.CharField(blank=True, max_length=190, null=True)),
('software_version', models.CharField(blank=True, max_length=190, null=True)),
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='pretixbase.Organizer')),
],
),
migrations.AlterUniqueTogether(
name='device',
unique_together={('organizer', 'device_id')},
),
migrations.AddField(
model_name='logentry',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Device'),
),
]

View File

@@ -0,0 +1,79 @@
# Generated by Django 2.1 on 2018-10-23 23:00
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0099_auto_20180912_1035'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_from_city',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_tax_id',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_vat_id',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_zipcode',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_city',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_company',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_name',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_vat_id',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_zipcode',
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2018-10-25 22:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0100_auto_20181023_2300'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='reverse_charge',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,96 @@
# Generated by Django 2.1 on 2018-10-17 00:24
import jsonfallback.fields
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations
from django_mysql.checks import mysql_connections
from django_mysql.utils import connection_is_mariadb
def set_attendee_name_parts(apps, schema_editor):
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
for op in OrderPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
op.save(update_fields=['attendee_name_parts'])
CartPosition = apps.get_model('pretixbase', 'CartPosition') # noqa
for op in CartPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
op.save(update_fields=['attendee_name_parts'])
InvoiceAddress = apps.get_model('pretixbase', 'InvoiceAddress') # noqa
for ia in InvoiceAddress.objects.exclude(name_cached=None).exclude(
name_cached__isnull=True).iterator():
ia.name_parts = {'_legacy': ia.name_cached}
ia.save(update_fields=['name_parts'])
def check_mysqlversion(apps, schema_editor):
errors = []
any_conn_works = False
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if connection_is_mariadb(conn) and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:
found = 'MariaDB ' + '.'.join(str(v) for v in conn.mysql_version)
elif hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (5, 7):
any_conn_works = True
else:
found = 'MySQL ' + '.'.join(str(v) for v in conn.mysql_version)
if conns and not any_conn_works:
raise ImproperlyConfigured(
'As of pretix 2.2, you need MySQL 5.7+ or MariaDB 10.2.7+ to run pretix. However, we detected a '
'database connection to {}'.format(found)
)
return errors
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0101_auto_20181025_2255'),
]
operations = [
migrations.RunPython(
check_mysqlversion, migrations.RunPython.noop
),
migrations.RenameField(
model_name='cartposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='orderposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='invoiceaddress',
old_name='name',
new_name='name_cached',
),
migrations.AddField(
model_name='cartposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
preserve_default=False,
),
migrations.RunPython(set_attendee_name_parts, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 2.1.1 on 2018-11-21 12:24
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0102_auto_20181017_0024'),
]
operations = [
migrations.AddField(
model_name='item',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web'], verbose_name='Sales channels'),
preserve_default=False,
),
migrations.AddField(
model_name='order',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
preserve_default=False,
),
]

View File

@@ -0,0 +1,62 @@
# Generated by Django 2.1.1 on 2018-11-14 15:26
import django.db.models.deletion
import django.db.models.manager
import jsonfallback.fields
from django.db import migrations, models
def change_refunded_to_canceled(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
Order.objects.filter(status='r').update(status='c')
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0103_auto_20181121_1224'),
]
operations = [
migrations.AlterModelManagers(
name='orderposition',
managers=[
('all', django.db.models.manager.Manager()),
],
),
migrations.AddField(
model_name='orderposition',
name='canceled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('cancellation', 'Cancellation fee'), ('other', 'Other fees'), ('giftcard', 'Gift card')], max_length=100),
),
migrations.AlterField(
model_name='orderposition',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='all_positions',
to='pretixbase.Order', verbose_name='Order'),
),
migrations.AlterModelManagers(
name='orderfee',
managers=[
('all', django.db.models.manager.Manager()),
],
),
migrations.AddField(
model_name='orderfee',
name='canceled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='orderfee',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='all_fees', to='pretixbase.Order', verbose_name='Order'),
),
migrations.RunPython(
change_refunded_to_canceled, migrations.RunPython.noop
)
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.1 on 2019-01-12 15:12
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0104_auto_20181114_1526'),
]
operations = [
migrations.AddField(
model_name='invoiceaddress',
name='beneficiary',
field=models.TextField(blank=True, verbose_name='Beneficiary'),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_beneficiary',
field=models.TextField(blank=True, null=True, verbose_name='Beneficiary'),
),
]

View File

@@ -0,0 +1,54 @@
# Generated by Django 2.1.5 on 2019-02-04 13:02
import django.db.migrations.operations.special
from django.db import migrations, models
def enable_notifications_for_everyone(apps, schema_editor):
NotificationSetting = apps.get_model('pretixbase', 'NotificationSetting')
User = apps.get_model('pretixbase', 'User')
create = []
for u in User.objects.iterator():
create.append(NotificationSetting(
user=u,
action_type='pretix.event.order.refund.requested',
event=None,
method='mail',
enabled=True
))
if len(create) > 200:
NotificationSetting.objects.bulk_create(create)
create.clear()
NotificationSetting.objects.bulk_create(create)
create.clear()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0105_auto_20190112_1512'), ('pretixbase', '0106_auto_20190118_1527'),
('pretixbase', '0107_auto_20190129_1337')]
dependencies = [
('pretixbase', '0104_auto_20181114_1526'),
]
operations = [
migrations.AddField(
model_name='invoiceaddress',
name='beneficiary',
field=models.TextField(blank=True, verbose_name='Beneficiary'),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_beneficiary',
field=models.TextField(blank=True, null=True, verbose_name='Beneficiary'),
),
migrations.RunPython(
code=enable_notifications_for_everyone,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='order',
name='datetime',
field=models.DateTimeField(db_index=True, verbose_name='Date'),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 2.1.5 on 2019-01-18 15:27
from django.db import migrations
def enable_notifications_for_everyone(apps, schema_editor):
NotificationSetting = apps.get_model('pretixbase', 'NotificationSetting')
User = apps.get_model('pretixbase', 'User')
create = []
for u in User.objects.iterator():
create.append(NotificationSetting(
user=u,
action_type='pretix.event.order.refund.requested',
event=None,
method='mail',
enabled=True
))
if len(create) > 200:
NotificationSetting.objects.bulk_create(create)
create.clear()
NotificationSetting.objects.bulk_create(create)
create.clear()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0105_auto_20190112_1512'),
]
operations = [
migrations.RunPython(enable_notifications_for_everyone, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.1.5 on 2019-01-29 13:37
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0106_auto_20190118_1527'),
]
operations = [
migrations.AlterField(
model_name='order',
name='datetime',
field=models.DateTimeField(db_index=True, verbose_name='Date'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.1.5 on 2019-02-01 15:27
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0107_auto_20190129_1337'),
]
operations = [
migrations.AddField(
model_name='item',
name='generate_tickets',
field=models.NullBooleanField(verbose_name='Allow ticket download'),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.1 on 2019-02-08 14:32
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0108_auto_20190201_1527'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='event_date_from',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.1.5 on 2019-02-19 12:45
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0109_auto_20190208_1432'),
]
operations = [
migrations.AlterField(
model_name='event',
name='plugins',
field=models.TextField(blank=True, default='', verbose_name='Plugins'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.1.5 on 2019-02-19 09:49
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0110_auto_20190219_1245'),
]
operations = [
migrations.AddField(
model_name='order',
name='testmode',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='event',
name='testmode',
field=models.BooleanField(default=False),
),
]

View File

@@ -2,6 +2,7 @@ from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin, CheckinList
from .devices import Device
from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,

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