Compare commits

..

102 Commits

Author SHA1 Message Date
Raphael Michel
027d28e646 Bump verstion to 3.1.0 2019-09-10 10:50:18 +02:00
Raphael Michel
babcf66a2e Update from Weblate (#1393)
Update from Weblate
2019-09-10 10:18:46 +02:00
Raphael Michel
20f9e88d61 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-10 08:16:51 +00:00
Raphael Michel
9594607a8c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-10 08:16:50 +00:00
Raphael Michel
ad3bcaf43a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-10 10:00:56 +02:00
Raphael Michel
2c4ee3b3c7 Replace U2F with WebAuthn (#1392)
* Replace U2F with WebAuthn

* Imports

* Fix backwards compatibility

* Add explanatory comment

* Fix tests
2019-09-10 09:58:31 +02:00
Raphael Michel
21451db412 Fix Greek VAT IDs 2019-09-10 09:46:00 +02:00
Raphael Michel
12b1f7d90e Fix #1374 -- Correct encoding of images 2019-09-10 09:39:16 +02:00
Raphael Michel
c3901c567e Correctly respect both attention flag in check-in list exports 2019-09-09 11:07:22 +02:00
Raphael Michel
0a3eddcd5c Merge pull request #1384 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-09 09:48:36 +02:00
Raphael Michel
dfa99cd325 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3219 of 3219 strings)

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

powered by weblate
2019-09-09 07:48:08 +00:00
Raphael Michel
62a040255e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3219 of 3219 strings)

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

powered by weblate
2019-09-09 07:48:07 +00:00
Maarten van den Berg
0e2b02c778 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (3216 of 3219 strings)

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

powered by weblate
2019-09-09 05:00:09 +00:00
Ture Gjørup
0f0ed90be9 Translated on translate.pretix.eu (Danish)
Currently translated at 61.2% (63 of 103 strings)

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

powered by weblate
2019-09-08 19:26:46 +00:00
Raphael Michel
b1e19d776c Fix exporter tests 2019-09-08 21:26:31 +02:00
Raphael Michel
0bcc784aaf Include attention flag and comment in order and check-in list exports 2019-09-08 20:52:15 +02:00
Raphael Michel
262fb82237 Add-ons should not have a seat 2019-09-06 11:54:18 +02:00
Raphael Michel
adbe959314 Merge pull request #1383 from pajowu/pajowu/no_test_data
Remove obsolete reference to test data
2019-09-05 23:04:15 +02:00
pajowu
02d0a68d57 Remove obsolete reference to test data 2019-09-05 19:29:04 +02:00
Raphael Michel
d6985123b4 Regenerate event CSS on plugins change 2019-09-04 15:39:23 +02:00
Raphael Michel
f7a356c340 Fixed bug in card extensions 2019-09-03 12:51:38 +02:00
Raphael Michel
c6265b4517 Fix allow_ignore_quota with bundled items 2019-09-03 12:25:22 +02:00
Raphael Michel
1ee352e114 Fix error in locale file 2019-09-03 11:59:05 +02:00
Raphael Michel
6c830a7d36 Do not enforce voucher constraints for bundled items 2019-09-03 11:51:29 +02:00
Raphael Michel
129c360fff Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-03 11:36:55 +02:00
Raphael Michel
b5c7ad92b6 Merge pull request #1379 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-03 11:36:19 +02:00
Raphael Michel
33efd8c157 order_overview: Allow to restrict to admission products 2019-09-03 11:34:00 +02:00
Bostjan Marusic
f318c8e017 Translated on translate.pretix.eu (Slovenian)
Currently translated at 21.9% (702 of 3208 strings)

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

powered by weblate
2019-09-03 09:22:31 +00:00
Bostjan Marusic
319334706d Translated on translate.pretix.eu (Slovenian)
Currently translated at 18.8% (602 of 3208 strings)

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

powered by weblate
2019-09-03 09:22:31 +00:00
Raphael Michel
1a25138bef PDF editor: Allow to easily change the page size 2019-09-03 11:22:21 +02:00
Raphael Michel
daa5383b89 Fix a layout animation issue in the sidebar 2019-09-02 18:07:10 +02:00
Raphael Michel
31333280d2 Fix download link for addons on ticket pages 2019-08-31 11:59:17 +02:00
Raphael Michel
b78f8d70e8 Add serializable to spelling whitelist 2019-08-30 17:46:15 +02:00
Raphael Michel
a847538a2e Merge pull request #1367 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-08-30 17:40:38 +02:00
Raphael Michel
e3ef9eba9e Widget: Fix invalid coloring of days in mobile calendar 2019-08-30 17:37:37 +02:00
Bostjan Marusic
9e4a4402fb Translated on translate.pretix.eu (Slovenian)
Currently translated at 15.6% (502 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Bostjan Marusic
96ed9f5cf5 Translated on translate.pretix.eu (Slovenian)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
oocf
7216cebce5 Translated on translate.pretix.eu (Spanish)
Currently translated at 94.3% (3025 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Bostjan Marusic
a3892fd4de Translated on translate.pretix.eu (Slovenian)
Currently translated at 10.7% (343 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
saad91
7718462528 Translated on translate.pretix.eu (Arabic)
Currently translated at 0.2% (8 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
60b20829f3 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
e11d03f418 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Tobias Sundgren
69e4db58fd Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Tobias Sundgren
9f27a84f52 Translated on translate.pretix.eu (Swedish)
Currently translated at 1.0% (33 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
e5c204dc95 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Raphael Michel
7fc7dd0163 Allow to print question answers on invoices 2019-08-30 17:24:57 +02:00
Raphael Michel
aa99dbc830 Add payment provider specific details to the API 2019-08-30 17:04:22 +02:00
Raphael Michel
e3a4ec93fc Fix user log that always shows empty 2019-08-30 13:01:35 +02:00
Raphael Michel
67da6a18a8 Add missing signal to documentation 2019-08-30 12:58:20 +02:00
Raphael Michel
adc4128f9f Fix exceptions introduced in last commit 2019-08-30 11:56:04 +02:00
Raphael Michel
eed217262f Add signal global_email_filter 2019-08-30 11:02:59 +02:00
Raphael Michel
4bae824a03 Add user argument to email_filter 2019-08-30 11:02:59 +02:00
Martin Gross
be1a1f7995 Fix widget-URL for subevents 2019-08-30 10:41:08 +02:00
Martin Gross
f4b81aa032 Fix import sorting 2019-08-28 17:17:42 +02:00
Martin Gross
2559439c4e Fix eMail-previews for per-attendant emails 2019-08-28 16:48:59 +02:00
Martin Gross
fce9117dfd Fix explanation on per-attendant emails 2019-08-28 16:47:54 +02:00
Martin Gross
4c8dc8f31c Show attendee-name of item - even if item is an add-on 2019-08-28 09:04:41 +02:00
Martin Gross
b4f69fb13f Move question hint-icons into their own columns 2019-08-27 15:43:02 +02:00
Martin Gross
2aed894bd4 Hide hidden questions from users in presale 2019-08-27 15:31:22 +02:00
Martin Gross
2ff5416afb Show hints for hidden/check-in questions in question overview 2019-08-27 15:19:28 +02:00
Raphael Michel
59f7098a70 Clean up duplicate seats when generating seats 2019-08-26 16:57:48 +02:00
Raphael Michel
83dd865b78 Fix crash when de-selecting all languages 2019-08-26 16:44:20 +02:00
Raphael Michel
ebf411b7a0 Fail gracefully if seats exist multiple times 2019-08-26 16:33:35 +02:00
Raphael Michel
733a4ce8f4 Fix seatingframe for subevents 2019-08-26 16:27:43 +02:00
Raphael Michel
ad94263374 Fix i18n of OrderError 2019-08-17 14:07:56 +02:00
Raphael Michel
102772ec55 Disable Sentry in shell sessions 2019-08-17 14:05:06 +02:00
Raphael Michel
9a826b694f Fix a type error PRETIXEU-1BF 2019-08-17 12:39:38 +02:00
Raphael Michel
bcf8e9cd04 Add short option to get_date_*_display 2019-08-16 16:44:01 +02:00
Raphael Michel
200ce93bb4 Prevent autoresponders 2019-08-14 15:09:19 +02:00
Raphael Michel
0582a4d9e5 Lazy widgets: Fix missing ID 2019-08-14 09:57:55 +02:00
Raphael Michel
1cbab04108 Docs: Add API guide on custom checkout 2019-08-14 09:22:58 +02:00
Raphael Michel
98c18b162f Order creation API: Add support for reverse charge 2019-08-14 09:22:58 +02:00
Raphael Michel
d972cd4c49 Add new bundled plugin "returnurl" 2019-08-14 09:22:58 +02:00
Raphael Michel
985f354293 Order API: Add order URL 2019-08-14 09:22:58 +02:00
Raphael Michel
9c23216bd1 Order creation API: Do not allow empty orders 2019-08-14 09:22:58 +02:00
Raphael Michel
f8bf44c262 Order payment flow: Allow to be used as an iframe session start 2019-08-14 09:22:58 +02:00
Raphael Michel
6badfdf576 Order API: Expose explicit URL of payment steps 2019-08-14 09:22:58 +02:00
Raphael Michel
c2eba21359 Order creation API: Allow to create orders without payment provider 2019-08-14 09:22:58 +02:00
Raphael Michel
5cda04a994 Order creation API: Allow to pass vouchers 2019-08-14 09:22:58 +02:00
Raphael Michel
bc9d8f5bd8 Order creation API: Do not consume expired carts 2019-08-14 09:22:58 +02:00
Raphael Michel
9d6ff20191 Order creation API: Allow to auto-calculate prices 2019-08-14 09:22:58 +02:00
Raphael Michel
82684e6df3 Order creation API: Allow to send emails 2019-08-14 09:22:58 +02:00
Martin Gross
d681ae4dce Only output GA if event is using seating 2019-08-13 13:26:44 +02:00
Martin Gross
213e724e18 Include Seating in default PDF ticket template 2019-08-13 13:26:21 +02:00
Raphael Michel
6e0b80706c Badge exporter: Exclude add-on positions by default 2019-08-13 10:11:24 +02:00
Raphael Michel
5363f4206e Lazy-load dashboard widgets 2019-08-12 12:19:02 +02:00
Raphael Michel
9bdb715874 Re-order list of devices 2019-08-12 11:48:50 +02:00
Raphael Michel
90a9709838 Redirect to profile after creating a new organizer 2019-08-12 11:45:54 +02:00
Raphael Michel
669b438c91 Allow to disables fee in stats module 2019-08-09 15:02:59 +02:00
Raphael Michel
a1353b3773 Add a note on Google Analytics in the widget documentation 2019-08-09 15:02:59 +02:00
Martin Gross
f5c611982a Do not localize date, time, datetime in csv/excel exports 2019-08-09 13:40:32 +02:00
Raphael Michel
e9ab56486a Fix broken test case 2019-08-09 12:53:56 +02:00
Raphael Michel
74bc495eb7 PDF exporter: Deal with duplicate question answers 2019-08-09 12:36:01 +02:00
Raphael Michel
b0b0f7474d Allow state selection without JavaScript 2019-08-09 12:13:09 +02:00
Raphael Michel
663ff60d0a Update from Weblate (#1363)
Update from Weblate
2019-08-09 11:51:59 +02:00
Raphael Michel
7bafb0bc76 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-09 08:26:33 +00:00
Raphael Michel
1ab225f40a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-09 08:26:33 +00:00
Raphael Michel
3b09456755 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-08-09 09:58:52 +02:00
Raphael Michel
d919605d79 Invoice addresses: Ask for a state in some countries (#1362)
* Invoice addresses: Ask for a state in some countries

* API, tests, noscript

* Fix shredder tests

* Add test for addresses with long state names
2019-08-09 09:55:46 +02:00
Raphael Michel
547f71aac6 Widget builder: explicit encoding for file reading 2019-08-08 19:43:44 +02:00
Raphael Michel
191729c07a Remove "3DS mode" setting as it no longer has an effect 2019-08-08 19:25:31 +02:00
Raphael Michel
8f0a5d859d Bump version to 3.1.0.dev0 2019-08-08 10:48:59 +02:00
157 changed files with 25986 additions and 22199 deletions

View File

@@ -0,0 +1,132 @@
Creating an external checkout process
=====================================
Occasionally, we get asked whether it is possible to just use pretix' powerful backend as a ticketing engine but use
a fully-customized checkout process that only communicates via the API. This is possible, but with a few limitations.
If you go down this route, you will miss out on many of pretix features and safeguards, as well as the added flexibility
by most of pretix' plugins. We strongly recommend to talk this through with us before you decide this is the way to go.
However, this is really useful if you need to tightly integrate pretix into existing web applications that e.g. control
the pricing of your products in a way that cannot be mapped to pretix' product structures.
Creating orders
---------------
After letting your user select the products to buy in your application, you should create a new order object inside
pretix. Below, you can see an example of such an order, but most fields are optional and there are some more features
supported. Read :ref:`rest-orders-create` to learn more about this endpoint.
Please note that this endpoint assumes trustworthy input for the most part. By default, the endpoint checks that
you do not exceed any quotas, do not sell any seats twice, or do not use any redeemed vouchers. However, it will not
complain about violation of any other availability constraints, such as violation of time frames or minimum/maximum
amounts of either your product or event. Bundled products will not be added in automatically and fees will not be
calculated automatically.
.. sourcecode:: http
POST /api/v1/organizers/democon/events/3vjrh/orders/ HTTP/1.1
Host: test.pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Authorization: …
{
"email": "dummy@example.org",
"locale": "en",
"sales_channel": "web",
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": false,
"company": "Sample company",
"name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
"country": "US",
"state": "NY",
"internal_reference": "",
"vat_id": ""
},
"positions": [
{
"item": 21,
"variation": null,
"attendee_name_parts": {
"full_name": "Peter"
},
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
],
"fees": []
}
You will be returned a full order object that you can inspect, store, or use to build emails or confirmation pages for
the user. If you don't want to do that yourself, it will also contain the URL to our confirmation page in the ``url``
attribute. If you pass the ``"send_mail": true`` option, pretix will also send order confirmations for you.
Handling payments yourself
--------------------------
If you want to handle payments in your application, you can either just create the orders with status "paid" or you can
create them in "pending" state (the default) and later confirm the payment. We strongly advise to use the payment
provider ``"manual"`` in this case to avoid interference with payment code with pretix.
However, it is often unfeasible to implement the payment process yourself, and it also requires you to give up a
lot of pretix functionality, such as automatic refunds. Therefore, it is also possible to utilize pretix' native
payment process even in this case:
Using pretix payment providers
------------------------------
If you passed a ``payment_provider`` during order creation above, pretix will have created a payment object with state
``created`` that you can see in the returned order object. This payment object will have an attribute ``payment_url``
that you can use to let the user pay. For example, you could link or redirect to this page.
If you want the user to return to your application after the payment is complete, you can pass a query parameter
``return_url``. To prepare your event for this, open your event in the pretix backend and go to "Settings", then
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
worked! Your final URL could look like this::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
You can also embed this page in an ``<iframe>`` instead. Note, however, that this causes problems with some payment
methods such as PayPal which do not allow being opened in an iframe. pretix can partly work around these issues by
opening a new window, but will only to so if you also append an ``iframe=1`` parameter to the URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234&iframe=1
If you did **not** pass a payment method since you want us to ask the user which payment method they want to use, you
need to construct the URL from the ``url`` attribute of the order and the sub-path ``pay/change```. For example, you
would end up with the following URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/change
Of course, you can also use the ``iframe`` and ``return_url`` parameters here.
Optional: Cart reservations
---------------------------
Creating orders is an atomic operation: The order is either created as a whole or not at all. However, pretix'
built-in checkout automatically reserves tickets in a user's cart for a configurable amount of time to ensure users
will actually get their tickets once they started entering all their details. If you want a similar behavior in your
application, you need to create :ref:`rest-carts` through the API.
When creating your order, you can pass a ``consume_carts`` parameter with the cart ID(s) of your user. This way, the
quota reserved by the cart will be credited towards the order and the carts will be destroyed if (and only if) the
order creation succeeds.
Cart creation is currently even more limited than the order creation endpoints, as cart creation currently does not
support vouchers or automatic price calculation. If you require these features, please get in touch with us.

11
doc/api/guides/index.rst Normal file
View File

@@ -0,0 +1,11 @@
.. _`rest-api-guides`:
API Usage Guides
================
This part of the documentation contains how-to guides on some special use cases of our API.
.. toctree::
:maxdepth: 2
custom_checkout

View File

@@ -18,3 +18,4 @@ in functionality over time.
resources/index
ratelimit
webhooks
guides/index

View File

@@ -53,7 +53,9 @@ invoice_address object Invoice address
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
├ country string Customer country
├ country string Customer country code
├ state string Customer state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US.
├ internal_reference string Customer's internal reference to be printed on the invoice
├ vat_id string Customer VAT ID
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
@@ -82,6 +84,7 @@ require_approval boolean If ``true`` and
needs approval by an organizer before it can
continue. If ``true`` and the order is canceled,
this order has been denied by the event organizer.
url string The full URL to the order confirmation page
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
@@ -137,6 +140,12 @@ last_modified datetime Last modificati
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1:
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. _order-position-resource:
Order position resource
@@ -221,13 +230,27 @@ amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
payment_url string The URL where an user can continue with the payment (or ``null``)
details object Payment-specific information. This is a dictionary
with various fields that can be different between
payment providers, versions, payment states, etc. If
you read this field, you always need to be able to
deal with situations where values that you expect are
missing. Mostly, the field contains various IDs that
can be used for matching with other systems. If a
payment provider does not implement this feature,
the object is empty.
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. _order-payment-resource:
.. versionchanged:: 3.1
The attributes ``payment_url`` and ``details`` have been added.
.. _order-refund-resource:
Order refund resource
---------------------
@@ -288,6 +311,7 @@ List of all orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -310,7 +334,8 @@ List of all orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -373,6 +398,8 @@ List of all orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -431,6 +458,7 @@ Fetching individual orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -453,7 +481,8 @@ Fetching individual orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -516,6 +545,8 @@ Fetching individual orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -691,6 +722,8 @@ Deleting orders
: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.
.. _rest-orders-create:
Creating orders
---------------
@@ -716,23 +749,17 @@ Creating orders
* does not validate the number of items per order or the number of times an item can be included in an order
* does not validate any requirements related to add-on products
* does not validate any requirements related to add-on products and does not add bundled products automatically
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not check prices but believes any prices you send
* does not prevent you from buying items that can only be bought with a voucher
* does not calculate fees
* does not calculate fees automatically
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
module
* does not send order confirmations via email
* does not support reverse charge taxation
* does not support file upload questions
You can supply the following fields of the resource:
@@ -750,9 +777,9 @@ Creating orders
* ``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.
* ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
@@ -770,17 +797,22 @@ Creating orders
* ``zipcode``
* ``city``
* ``country``
* ``state``
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions``
* ``positionid`` (optional, see below)
* ``item``
* ``variation``
* ``price``
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts``
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -800,6 +832,8 @@ Creating orders
* ``tax_rule``
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
@@ -837,6 +871,7 @@ Creating orders
"zipcode": "12345",
"city": "Sample City",
"country": "UK",
"state": "",
"internal_reference": "",
"vat_id": ""
},
@@ -860,7 +895,7 @@ Creating orders
],
"subevent": null
}
],
]
}
**Example response**:
@@ -1546,6 +1581,8 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
]
@@ -1586,6 +1623,8 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}

View File

@@ -41,6 +41,8 @@ ask_during_checkin boolean If ``true``, th
the ticket instead.
hidden boolean If ``true``, the question will only be shown in the
backend.
print_on_invoice boolean If ``true``, the question will only be shown on
invoices.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use separate endpoint to modify this later.
@@ -80,6 +82,10 @@ dependency_value string An old version
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
Endpoints
---------
@@ -123,6 +129,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -192,6 +199,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -245,6 +253,7 @@ Endpoints
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_values": [],
"options": [
@@ -279,6 +288,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -352,6 +362,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],

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, register_sales_channels, register_global_settings, quota_availability
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
Order events
""""""""""""

View File

@@ -108,6 +108,8 @@ The provider class
.. automethod:: execute_refund
.. automethod:: api_payment_details
.. automethod:: shred_payment_info
.. autoattribute:: is_implicit

View File

@@ -65,7 +65,7 @@ Then, create the local database::
python manage.py migrate
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
created.
created.
If you want to see pretix in a different language than English, you have to compile our language
files::
@@ -81,8 +81,7 @@ To run the local development webserver, execute::
and head to http://localhost:8000/
As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
http://localhost:8000/control/ for the admin view.
.. note:: If you want the development server to listen on a different interface or
port (for example because you develop on `pretixdroid`_), you can check

View File

@@ -46,6 +46,7 @@ guid
hardcoded
hostname
idempotency
iframe
incrementing
inofficial
invalidations
@@ -104,6 +105,7 @@ screenshot
scss
searchable
selectable
serializable
serializers
serializers
sexualized

View File

@@ -274,6 +274,9 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
};
</script>
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
.. versionchanged:: 2.3

View File

@@ -22,3 +22,5 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
recursive-include pretix/plugins/returnurl/templates *
recursive-include pretix/plugins/returnurl/static *

View File

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

View File

@@ -82,6 +82,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available():

View File

@@ -219,7 +219,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value')
'hidden', 'dependency_value', 'print_on_invoice')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -1,6 +1,9 @@
import json
from collections import Counter
from decimal import Decimal
import pycountry
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy
from django_countries.fields import Country
@@ -14,13 +17,17 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, Voucher,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
)
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
from pretix.multidomain.urlreverse import build_absolute_uri
class CompatibleCountryField(serializers.Field):
@@ -41,8 +48,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceAddress
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')
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -57,6 +64,24 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
@@ -261,10 +286,33 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
})
class PaymentDetailsField(serializers.Field):
def to_representation(self, value: OrderPayment):
pp = value.payment_provider
if not pp:
return {}
return pp.api_payment_details(value)
class OrderPaymentSerializer(I18nAwareModelSerializer):
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
class Meta:
model = OrderPayment
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
'details')
class OrderRefundSerializer(I18nAwareModelSerializer):
@@ -275,6 +323,14 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
@@ -284,13 +340,15 @@ class OrderSerializer(I18nAwareModelSerializer):
refunds = OrderRefundSerializer(many=True, read_only=True)
payment_date = OrderPaymentDateField(source='*', read_only=True)
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
url = OrderURLField(source='*', read_only=True)
class Meta:
model = Order
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', 'sales_channel'
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
@@ -329,7 +387,7 @@ class OrderSerializer(I18nAwareModelSerializer):
}
try:
ia = instance.invoice_address
if iadata.get('vat_id') != ia.vat_id:
if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata:
ia.vat_id_validated = False
self.fields['invoice_address'].update(ia, iadata)
except InvoiceAddress.DoesNotExist:
@@ -437,11 +495,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers', 'seat')
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -515,7 +577,7 @@ class CompatibleJSONField(serializers.JSONField):
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=False)
positions = OrderPositionCreateSerializer(many=True, required=True)
fees = OrderFeeCreateSerializer(many=True, required=False)
status = serializers.ChoiceField(choices=(
('n', Order.STATUS_PENDING),
@@ -527,18 +589,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
payment_provider = serializers.CharField(required=True)
payment_provider = serializers.CharField(required=False, allow_null=True)
payment_info = CompatibleJSONField(required=False)
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail')
def validate_payment_provider(self, pp):
if pp is None:
return None
if pp not in self.context['event'].get_payment_providers():
raise ValidationError('The given payment provider is not known.')
return pp
@@ -608,10 +678,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
payment_provider = validated_data.pop('payment_provider')
payment_provider = validated_data.pop('payment_provider', None)
payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -630,6 +701,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
voucher_usage = Counter()
if consume_carts:
for cp in CartPosition.objects.filter(
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
@@ -641,6 +713,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] += 1
if cp.voucher:
voucher_usage[cp.voucher] -= 1
if cp.expires > now_dt:
if cp.seat:
free_seats.add(cp.seat)
@@ -648,8 +722,55 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
v = pos_data['voucher']
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
continue
if v.valid_until is not None and v.valid_until < now_dt:
errs[i]['voucher'] = [error_messages['voucher_expired']]
continue
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
if not force:
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
@@ -671,23 +792,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
]
for i, pos_data in enumerate(positions_data):
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
if any(errs):
raise ValidationError({'positions': errs})
@@ -695,38 +799,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
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'))
order.meta_info = "{}"
order.total = Decimal('0.00')
order.save()
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
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.')
elif validated_data.get('status') == Order.STATUS_PAID:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=payment_date,
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
@@ -738,9 +818,27 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
}
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
if addon_to:
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(
item=pos.item,
variation=pos.variation,
voucher=pos.voucher,
custom_price=None,
subevent=pos.subevent,
addon_to=pos.addon_to,
invoice_address=ia,
)
pos.price = price.gross
pos.tax_rate = price.rate
pos.tax_value = price.tax
pos.tax_rule = pos.item.tax_rule
else:
pos._calculate_tax()
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
@@ -750,12 +848,43 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for cp in delete_cps:
cp.delete()
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
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.')
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=payment_date,
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
return order

View File

@@ -41,7 +41,8 @@ from pretix.base.services.invoices import (
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.pricing import get_price
@@ -431,6 +432,7 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
@@ -445,8 +447,42 @@ class OrderViewSet(viewsets.ModelViewSet):
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
invoice = None
if gen_invoice:
generate_invoice(order, trigger_pdf=True)
invoice = generate_invoice(order, trigger_pdf=True)
if send_mail:
payment = order.payments.last()
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
not order.require_approval and payment.provider == "free"
)
if free_flow:
email_template = request.event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
if self.request.event.settings.mail_send_order_paid_attendee:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
payment._send_paid_mail_attendee(p, None)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

View File

@@ -9,7 +9,7 @@ from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition,
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -109,6 +109,8 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
yield headers
@@ -153,10 +155,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (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 '',
@@ -178,6 +181,8 @@ class OrderListExporter(MultiSheetListExporter):
row.append(', '.join([i.number for i in order.invoices.all()]))
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
yield row
def iterate_fees(self, form_data: dict):
@@ -208,7 +213,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
yield headers
@@ -243,10 +248,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
@@ -301,7 +307,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers.append(_('Sales channel'))
@@ -339,7 +345,12 @@ class OrderListExporter(MultiSheetListExporter):
]
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
else:
acache[a.question_id] = str(a)
for q in questions:
row.append(acache.get(q.pk, ''))
try:
@@ -358,10 +369,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row.append(order.sales_channel)
yield row
@@ -503,6 +515,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -552,6 +565,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
@@ -591,6 +605,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -630,6 +645,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,

View File

@@ -5,6 +5,7 @@ from decimal import Decimal
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
@@ -15,7 +16,9 @@ from django.db.models import QuerySet
from django.forms import Select
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, ugettext_lazy as _
from django.utils.translation import (
get_language, pgettext_lazy, ugettext_lazy as _,
)
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -24,8 +27,11 @@ from pretix.base.forms.widgets import (
TimePickerWidget, UploadedFileWidget,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.escapejson import escapejson_attr
@@ -356,8 +362,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference', 'beneficiary')
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
'vat_id', 'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
@@ -400,6 +406,30 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
cc = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif 'country' in self.initial:
cc = str(self.initial['country'])
elif self.instance and self.instance.country:
cc = str(self.instance.country)
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c
)
self.fields['state'].widget.is_required = True
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
@@ -446,6 +476,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not data.get('state'):
self.add_error('state', _('This field is required.'))
self.instance.name_parts = data.get('name_parts')
if all(
@@ -457,7 +491,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
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'):
if data.get('vat_id')[:2] != str(data.get('country')):
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
try:
result = vat_moss.id.validate(data.get('vat_id'))

View File

@@ -115,5 +115,5 @@ class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.1 on 2019-08-08 12:53
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0131_auto_20190729_1422'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_to_state',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoiceaddress',
name='state',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-08-30 15:13
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0132_auto_20190808_1253'),
]
operations = [
migrations.AddField(
model_name='question',
name='print_on_invoice',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-09-09 10:42
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0133_auto_20190830_1513'),
]
operations = [
migrations.CreateModel(
name='WebAuthnDevice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('confirmed', models.BooleanField(default=True)),
('credential_id', models.CharField(max_length=255, null=True)),
('rp_id', models.CharField(max_length=255, null=True)),
('icon_url', models.CharField(max_length=255, null=True)),
('ukey', models.TextField(null=True)),
('pub_key', models.TextField(null=True)),
('sign_count', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View File

@@ -1,5 +1,5 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .auth import U2FDevice, User, WebAuthnDevice
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin, CheckinList
from .devices import Device

View File

@@ -1,5 +1,9 @@
import binascii
import json
from datetime import timedelta
from urllib.parse import urlparse
import webauthn
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -13,6 +17,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
@@ -176,6 +183,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=self,
locale=self.locale
)
except SendMailException:
@@ -191,9 +199,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
},
None, locale=self.locale
None, locale=self.locale, user=self
)
@property
def top_logentries(self):
return self.all_logentries
@property
def all_logentries(self):
from pretix.base.models import LogEntry
@@ -375,3 +387,49 @@ class StaffSessionAuditLog(models.Model):
class U2FDevice(Device):
json_data = models.TextField()
@property
def webauthnuser(self):
d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This
# is based on the following example:
# https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples
pub_key = pub_key_from_der(websafe_decode(d['publicKey'].replace('+', '-').replace('/', '_')))
pub_key = binascii.unhexlify(
'A5010203262001215820{:064x}225820{:064x}'.format(
pub_key.public_numbers().x, pub_key.public_numbers().y
)
)
return webauthn.WebAuthnUser(
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device):
credential_id = models.CharField(max_length=255, null=True, blank=True)
rp_id = models.CharField(max_length=255, null=True, blank=True)
icon_url = models.CharField(max_length=255, null=True, blank=True)
ukey = models.TextField(null=True)
pub_key = models.TextField(null=True)
sign_count = models.IntegerField(default=0)
@property
def webauthnuser(self):
return webauthn.WebAuthnUser(
self.ukey,
self.user.email,
str(self.user),
settings.SITE_URL,
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)

View File

@@ -65,7 +65,7 @@ class EventMixin:
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_from_display(self, tz=None, show_times=True) -> str:
def get_date_from_display(self, tz=None, show_times=True, short=False) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
@@ -73,7 +73,7 @@ class EventMixin:
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
)
def get_time_from_display(self, tz=None) -> str:
@@ -86,7 +86,7 @@ class EventMixin:
self.date_from.astimezone(tz), "TIME_FORMAT"
)
def get_date_to_display(self, tz=None) -> str:
def get_date_to_display(self, tz=None, short=False) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
@@ -97,7 +97,7 @@ class EventMixin:
return ""
return _date(
self.date_to.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
@@ -608,22 +608,24 @@ class Event(EventMixin, LoggedModel):
question_map=question_map
)
def get_payment_providers(self) -> dict:
def get_payment_providers(self, cached=False) -> dict:
"""
Returns a dictionary of initialized payment providers mapped by their identifiers.
"""
from ..signals import register_payment_providers
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
if not cached or not hasattr(self, '_cached_payment_providers'):
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
self._cached_payment_providers = OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
return self._cached_payment_providers
def get_html_mail_renderer(self):
"""
@@ -824,18 +826,24 @@ class Event(EventMixin, LoggedModel):
def enable_plugin(self, module, allow_restricted=False):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module not in plugins_active:
plugins_active.append(module)
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
regenerate_css.apply_async(args=(self.pk,))
def disable_plugin(self, module):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module in plugins_active:
plugins_active.remove(module)
self.set_active_plugins(plugins_active)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod
def clean_has_subevents(event, has_subevents):
if event is not None and event.has_subevents is not None:

View File

@@ -1,6 +1,7 @@
import string
from decimal import Decimal
import pycountry
from django.db import DatabaseError, models, transaction
from django.db.models import Max
from django.db.models.functions import Cast
@@ -11,6 +12,8 @@ from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
@@ -90,6 +93,7 @@ class Invoice(models.Model):
invoice_to_street = models.TextField(null=True)
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = CountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
@@ -140,11 +144,21 @@ class Invoice(models.Model):
def address_invoice_to(self):
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
return self.invoice_to
state_name = ""
if self.invoice_to_state:
state_name = self.invoice_to_state
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
).name
parts = [
self.invoice_to_company,
self.invoice_to_name,
self.invoice_to_street,
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
((self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or "") + " " + (state_name or "")).strip(),
self.invoice_to_country.name if self.invoice_to_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])

View File

@@ -982,6 +982,7 @@ class Question(LoggedModel):
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
event = models.ForeignKey(
Event,
@@ -1033,6 +1034,10 @@ class Question(LoggedModel):
help_text=_('This question will only show up in the backend.'),
default=False
)
print_on_invoice = models.BooleanField(
verbose_name=_('Print answer on invoices'),
default=False
)
dependency_question = models.ForeignKey(
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)

View File

@@ -9,6 +9,7 @@ from decimal import Decimal
from typing import Any, Dict, List, Union
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.db import models, transaction
@@ -696,7 +697,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -736,7 +737,7 @@ class Order(LockModel, LoggedModel):
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position
position=position, auto_email=auto_email
)
except SendMailException:
raise
@@ -1195,7 +1196,7 @@ class OrderPayment(models.Model):
"""
Cached access to an instance of the payment provider in use.
"""
return self.order.event.get_payment_providers().get(self.provider)
return self.order.event.get_payment_providers(cached=True).get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
@@ -2019,6 +2020,7 @@ class InvoiceAddress(models.Model):
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id_validated = models.BooleanField(default=False)
@@ -2045,6 +2047,22 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
@property
def name(self):
if not self.name_parts:

View File

@@ -85,6 +85,12 @@ EU_CURRENCIES = {
}
def cc_to_vat_prefix(country_code):
if country_code == 'GR':
return 'EL'
return country_code
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
name = I18nCharField(

View File

@@ -657,6 +657,15 @@ class BasePaymentProvider:
obj.info = '{}'
obj.save(update_fields=['info'])
def api_payment_details(self, payment: OrderPayment):
"""
Will be called to populate the ``details`` parameter of the payment in the REST API.
:param payment: The payment in question.
:return: A serializable dictionary
"""
return {}
class PaymentException(Exception):
pass
@@ -720,6 +729,12 @@ class BoxOfficeProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"pos_id": payment.info_data.get('pos_id', None),
"receipt_id": payment.info_data.get('receipt_id', None),
}
def payment_control_render(self, request, payment) -> str:
if not payment.info:
return
@@ -864,6 +879,11 @@ class OffsettingProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"orders": payment.info_data.get('orders', []),
}
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))

View File

@@ -32,7 +32,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition, QuestionAnswer
from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -241,12 +241,14 @@ DEFAULT_VARIABLES = OrderedDict((
("seat", {
"label": _("Seat: Full name"),
"editor_sample": _("Ground floor, Row 3, Seat 4"),
"evaluate": lambda op, order, ev: str(op.seat if op.seat else _('General admission'))
"evaluate": lambda op, order, ev: str(op.seat if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
}),
("seat_zone", {
"label": _("Seat: zone"),
"editor_sample": _("Ground floor"),
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else _('General admission'))
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
}),
("seat_row", {
"label": _("Seat: row"),
@@ -268,10 +270,10 @@ def variables_from_questions(sender, *args, **kwargs):
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
a = [a for a in op.answers.all() if a.question_id == question_id][0]
else:
a = op.answers.get(question_id=question_id)
a = op.answers.filter(question_id=question_id).first()
if not a:
return ""
return str(a).replace("\n", "<br/>\n")
except QuestionAnswer.DoesNotExist:
return ""
except IndexError:
return ""

View File

@@ -226,11 +226,15 @@ class CartManager:
def _check_item_constraints(self, op):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
):
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.active):
raise CartError(error_messages['unavailable'])
@@ -432,6 +436,8 @@ class CartManager:
seat = (subevent or self.event).seats.get(seat_guid=i.get('seat'))
except Seat.DoesNotExist:
raise CartError(error_messages['seat_invalid'])
except Seat.MultipleObjectsReturned:
raise CartError(error_messages['seat_invalid'])
i['item'] = seat.product_id
if i['item'] not in self._items_cache:
self._update_items_cache([i['item']], [i['variation']])
@@ -612,7 +618,7 @@ class CartManager:
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=cp.seat
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None
)
self._check_item_constraints(op)
operations.append(op)
@@ -791,8 +797,9 @@ class CartManager:
for b in op.bundled:
b_quotas = list(b.quotas)
if not b_quotas:
err = err or error_messages['unavailable']
available_count = 0
if not op.voucher or not op.voucher.allow_ignore_quota:
err = err or error_messages['unavailable']
available_count = 0
continue
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
if b_quota_available_count < b.count:

View File

@@ -73,12 +73,15 @@ def build_invoice(invoice: Invoice) -> Invoice:
addr_template = pgettext("invoice", """{i.company}
{i.name}
{i.street}
{i.zipcode} {i.city}
{i.zipcode} {i.city} {state}
{country}""")
invoice.invoice_to = addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old
).strip()
invoice.invoice_to = "\n".join(
a.strip() for a in addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old,
state=ia.state_for_address
).split("\n") if a.strip()
)
invoice.internal_reference = ia.internal_reference
invoice.invoice_to_company = ia.company
invoice.invoice_to_name = ia.name
@@ -86,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_to_zipcode = ia.zipcode
invoice.invoice_to_city = ia.city
invoice.invoice_to_country = ia.country
invoice.invoice_to_state = ia.state
invoice.invoice_to_beneficiary = ia.beneficiary
if ia.vat_id:
@@ -125,7 +129,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
addon_c=Count('addons')
).order_by('positionid', 'id')
).prefetch_related('answers', 'answers__question').order_by('positionid', 'id')
)
reverse_charge = False
@@ -142,6 +146,16 @@ def build_invoice(invoice: Invoice) -> Invoice:
desc = " + " + desc
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
for answ in p.answers.all():
if not answ.question.print_on_invoice:
continue
desc += "<br />{}{} {}".format(
answ.question.question,
"" if str(answ.question.question).endswith("?") else ":",
str(answ)
)
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create(

View File

@@ -4,7 +4,6 @@ import os
import re
import smtplib
import warnings
from email.encoders import encode_noop
from email.mime.image import MIMEImage
from email.utils import formataddr
from typing import Any, Dict, List, Union
@@ -24,12 +23,12 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition,
Event, Invoice, InvoiceAddress, Order, OrderPosition, User,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -51,7 +50,7 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False):
invoices: list=None, attach_tickets=False, auto_email=True, user=None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -86,6 +85,10 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param attach_tickets: Whether to attach tickets to this email, if they are available to download.
:param auto_email: Whether this email is auto-generated
:param user: The user this email is sent to
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -93,6 +96,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
return
headers = headers or {}
if auto_email:
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
headers['Auto-Submitted'] = 'auto-generated'
with language(locale):
if isinstance(context, dict) and event:
@@ -209,7 +215,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
invoices=[i.pk for i in invoices] if invoices and not position else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets
attach_tickets=attach_tickets,
user=user.pk if user else None
)
if invoices:
@@ -224,13 +231,16 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
invoices: List[int]=None, order: int=None, attach_tickets=False, user=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
html_with_cid, cid_images = replace_images_with_cid_paths(html)
email = attach_cid_images(email, cid_images, verify_ssl=True)
email.attach_alternative(html_with_cid, "text/html")
if user:
user = User.objects.get(pk=user)
if event:
with scopes_disabled():
event = Event.objects.get(id=event)
@@ -292,7 +302,9 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
}
)
email = email_filter.send_chained(event, 'message', message=email, order=order)
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
try:
backend.send_messages([email])
@@ -380,12 +392,27 @@ def attach_cid_images(msg, cid_images, verify_ssl=True):
return msg
def encoder_linelength(msg):
"""
RFC1341 mandates that base64 encoded data may not be longer than 76 characters per line
https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html section 5.2
"""
orig = msg.get_payload(decode=True).replace(b"\n", b"").replace(b"\r", b"")
max_length = 76
pieces = []
for i in range(0, len(orig), max_length):
chunk = orig[i:i + max_length]
pieces.append(chunk)
msg.set_payload(b"\r\n".join(pieces))
def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
try:
if image_src.startswith('data:image/'):
image_type, image_content = image_src.split(',', 1)
image_type = re.findall(r'data:image/(\w+);base64', image_type)[0]
mime_image = MIMEImage(image_content, _subtype=image_type, _encoder=encode_noop)
mime_image = MIMEImage(image_content, _subtype=image_type, _encoder=encoder_linelength)
mime_image.add_header('Content-Transfer-Encoding', 'base64')
elif image_src.startswith('data:'):
logger.exception("ERROR creating MIME element %s[%s]" % (cid_id, image_src))

View File

@@ -120,4 +120,5 @@ def send_notification_mail(notification: Notification, user: User):
'html': body_html,
'sender': settings.MAIL_FROM,
'headers': {},
'user': user.pk
})

View File

@@ -403,7 +403,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
class OrderError(LazyLocaleException):
pass
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def _check_date(event: Event, now_dt: datetime):
@@ -501,12 +509,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
if cp.seat:
seats_seen.add(cp.seat)
if cp.item.require_voucher and cp.voucher is None:
if cp.item.require_voucher and cp.voucher is None and not cp.is_bundled:
delete(cp)
err = err or error_messages['voucher_required']
break
if cp.item.hide_without_voucher and (cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)):
if cp.item.hide_without_voucher and (
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
) and not cp.is_bundled:
delete(cp)
cp.delete()
err = error_messages['voucher_required']

View File

@@ -25,10 +25,12 @@ def validate_plan_change(event, subevent, plan):
def generate_seats(event, subevent, plan, mapping):
current_seats = {
s.seat_guid: s for s in
event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent)
}
current_seats = {}
for s in event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent):
if s.seat_guid in current_seats:
s.delete() # Duplicates should not exist
else:
current_seats[s.seat_guid] = s
def update(o, a, v):
if getattr(o, a) != v:

View File

@@ -76,7 +76,8 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -87,6 +88,9 @@ def order_overview(
qs = OrderPosition.all
if subevent:
qs = qs.filter(subevent=subevent)
if admission_only:
qs = qs.filter(item__admission=True)
items = items.filter(admission=True)
if date_from and isinstance(date_from, date):
date_from = make_aware(datetime.combine(
@@ -189,7 +193,7 @@ def order_overview(
payment_cat_obj.name = _('Fees')
payment_items = []
if not subevent:
if not subevent and fees:
qs = OrderFee.all.filter(
order__event=event
).annotate(

View File

@@ -880,6 +880,19 @@ PERSON_NAME_SCHEMES = OrderedDict([
},
}),
])
COUNTRIES_WITH_STATE_IN_ADDRESS = {
# Source: http://www.bitboost.com/ref/international-address-formats.html
# This is not a list of countries that *have* states, this is a list of countries where states
# are actually *used* in postal addresses. This is obviously not complete and opinionated.
# Country: [(List of subdivision types as defined by pycountry), (short or long form to be used)]
'AU': (['State', 'Territory'], 'short'),
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State'], 'long'),
'MX': (['State', 'Federal District'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
}
settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -139,6 +139,24 @@ class EventPluginSignal(django.dispatch.Signal):
return sorted_list
class GlobalSignal(django.dispatch.Signal):
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
second receiver and so on. The return value of the last receiver is returned by this method.
"""
response = named.get(chain_kwarg_name)
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response
for receiver in self._live_receivers(sender):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
class DeprecatedSignal(django.dispatch.Signal):
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
@@ -500,7 +518,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
"""
email_filter = EventPluginSignal(
providing_args=['message', 'order']
providing_args=['message', 'order', 'user']
)
"""
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
@@ -510,8 +528,24 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
"""
global_email_filter = GlobalSignal(
providing_args=['message', 'order', 'user']
)
"""
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you.
This signal is called on all events and even if there is no known event. ``sender`` is an event or None.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
"""
layout_text_variables = EventPluginSignal()
"""

View File

@@ -0,0 +1,16 @@
import pycountry
from django.http import JsonResponse
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
cc = request.GET.get("country", "DE")
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return JsonResponse({'data': []})
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return JsonResponse({'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
]})

View File

@@ -160,7 +160,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
def positions(self):
qqs = self.request.event.questions.all()
if self.only_user_visible:
qqs = qqs.filter(ask_during_checkin=False)
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
return list(self.order.positions.select_related(
'item', 'variation'
).prefetch_related(

View File

@@ -395,8 +395,8 @@ class EventSettingsForm(SettingsForm):
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation only to the one primary email address, not to the "
"per-attendee addresses."),
"pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the E-mail settings."),
required=False
)
attendee_emails_required = forms.BooleanField(
@@ -534,7 +534,7 @@ class EventSettingsForm(SettingsForm):
def clean(self):
data = super().clean()
if data['locale'] not in data['locales']:
if 'locales' in data and data['locale'] not in data['locales']:
raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).')
})

View File

@@ -94,7 +94,8 @@ class QuestionForm(I18nModelForm):
'identifier',
'items',
'dependency_question',
'dependency_values'
'dependency_values',
'print_on_invoice',
]
widgets = {
'items': forms.CheckboxSelectMultiple(

View File

@@ -4,7 +4,7 @@
{% load static %}
{% load compress %}
{% block content %}
<form class="form-signin" action="" method="post" id="u2f-form">
<form class="form-signin" action="" method="post" id="webauthn-form">
{% csrf_token %}
<h3>{% trans "Welcome back!" %}</h3>
<p>
@@ -12,14 +12,14 @@
</p>
<div class="form-group">
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
type="text" required="required" autofocus="autofocus" id="u2f-response">
type="text" required="required" autofocus="autofocus" id="webauthn-response">
</div>
<div class="sr-only alert alert-danger" id="u2f-error">
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
<div class="sr-only alert alert-danger" id="webauthn-error">
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
</div>
{% if jsondata %}
<p><small>
{% trans "Alternatively, connect your U2F device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
{% trans "Alternatively, connect your WebAuthn device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
</small></p>
{% endif %}
<div class="form-group buttons">
@@ -29,14 +29,14 @@
</div>
</form>
{% if jsondata %}
<script type="text/json" id="u2f-login">
<script type="text/json" id="webauthn-login">
{{ jsondata|safe }}
</script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
{% endcompress %}
{% endblock %}

View File

@@ -47,6 +47,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/dashboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>

View File

@@ -105,7 +105,7 @@
{% endif %}
</td>
<td>
{% if e.addon_to %}
{% if e.addon_to and not e.attendee_name %}
{{ e.addon_to.attendee_name }}
{% elif e.attendee_name %}
{{ e.attendee_name }}

View File

@@ -95,18 +95,30 @@
{% endif %}
<div class="dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
<div class="widget-container widget-{{ w.display_size|default:"small" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
{% if w.url %}{# backwards compatibility #}
<a href="{{ w.url }}" class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a>
{% elif w.link %}
<a href="{{ w.link }}" class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x´"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a>
{% else %}
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -112,6 +112,7 @@
{% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %}
{% bootstrap_field form.hidden layout="control" %}
{% bootstrap_field form.print_on_invoice layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_dependency_question">

View File

@@ -31,6 +31,9 @@
<tr>
<th>{% trans "Question" %}</th>
<th>{% trans "Type" %}</th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
@@ -44,10 +47,21 @@
</td>
<td>
{{ q.get_type_display }}
</td>
<td>
{% if q.required %}
<span class="fa fa-exclamation-circle text-muted"
data-toggle="tooltip" title="{% trans "Required question" %}">
</span>
<span class="fa fa-exclamation-circle text-muted" data-toggle="tooltip" title="{% trans "Required question" %}"></span>
{% endif %}
</td>
<td>
{% if q.ask_during_checkin %}
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
{% endif %}
</td>
<td>
{% if q.hidden %}
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
{% endif %}
</td>
<td>

View File

@@ -663,6 +663,10 @@
<dd>{{ order.invoice_address.zipcode }} {{ order.invoice_address.city }}</dd>
<dt>{% trans "Country" %}</dt>
<dd>{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}</dd>
{% if order.invoice_address.state %}
<dt>{% trans "State" context "address" %}</dt>
<dd>{{ order.invoice_address.state_name }}</dd>
{% endif %}
{% if request.event.settings.invoice_address_vatid %}
<dt>{% trans "VAT ID" %}</dt>
<dd>

View File

@@ -46,7 +46,7 @@
{% if subevent_warning %}
<div class="alert alert-info">
{% blocktrans trimmed context "subevent" %}
If you select a single date, payment method fees will not be listed here as it might not be clear which
If you select a single date, fees will not be listed here as it might not be clear which
date they belong to.
{% endblocktrans %}
</div>

View File

@@ -42,7 +42,7 @@
</thead>
<tbody>
{% for d in devices %}
<tr>
<tr {% if d.revoked %}class="text-muted"{% endif %}>
<td>
{{ d.device_id }}
</td>

View File

@@ -175,22 +175,33 @@
<div class="row control-group pdf-info">
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control" disabled>
<input type="number" id="pdf-info-width" class="input-block-level form-control">
</div>
<div class="col-sm-6">
<label>{% trans "Height (mm)" %}</label><br>
<input type="number" id="pdf-info-height" class="input-block-level form-control" disabled>
<input type="number" id="pdf-info-height" class="input-block-level form-control">
</div>
</div>
<div class="row control-group pdf-info">
<div class="col-sm-12">
<label>{% trans "Background PDF" %}</label><br>
<span class="btn btn-default fileinput-button">
<p>
<button class="btn btn-default background-button" id="pdf-empty">
{% trans "Create empty background" %}
</button>
</p>
<span class="btn btn-default fileinput-button background-button">
<i class="fa fa-upload"></i>
<span>{% trans "Upload new background" %}</span>
<span>{% trans "Upload custom background" %}</span>
<input id="fileupload" type="file" name="background">
</span>
</div>
<div class="col-sm-12 help-inline">
<p>
After you changed the page size, you need to create a new empty background. If you
want to use a custom background, it already needs to have the correct size.
</p>
</div>
</div>
<div class="row control-group position">
<div class="col-sm-6">

View File

@@ -6,13 +6,13 @@
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
{% block content %}
<h1>{% trans "Add a two-factor authentication device" %}</h1>
<p id="u2f-progress">
<p id="webauthn-progress">
<span class="fa fa-cog fa-spin"></span>
{% trans "Please connect your U2F device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
{% trans "Please connect your WebAuthn device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
</p>
<form class="form form-inline" method="post" action="" id="u2f-form">
<form class="form form-inline" method="post" action="" id="webauthn-form">
{% csrf_token %}
<input type="hidden" id="u2f-response" name="token" class="form-control" required="required">
<input type="hidden" id="webauthn-response" name="token" class="form-control" required="required">
<p>
<label>
<input type="checkbox" name="activate" checked="checked" value="on">
@@ -22,16 +22,16 @@
<button class="btn btn-primary sr-only" type="submit"></button>
</form>
<div class="sr-only alert alert-danger" id="u2f-error">
<div class="sr-only alert alert-danger" id="webauthn-error">
{% trans "Device registration failed." %}
</div>
<script type="text/json" id="u2f-enroll">
<script type="text/json" id="webauthn-enroll">
{{ jsondata|safe }}
</script>
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
{% endcompress %}
{% endblock %}

View File

@@ -78,6 +78,8 @@
</a>
{% if d.devicetype == "totp" %}
<span class="fa fa-mobile"></span>
{% elif d.devicetype == "webauthn" %}
<span class="fa fa-usb"></span>
{% elif d.devicetype == "u2f" %}
<span class="fa fa-usb"></span>
{% endif %}

View File

@@ -10,6 +10,7 @@
{% trans "Account history" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=user %}
{% include "pretixcontrol/includes/logs.html" with obj=fakeobj %}
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -4,7 +4,7 @@
{% load compress %}
{% load static %}
{% block content %}
<form class="form-signin" id="u2f-form" action="" method="post">
<form class="form-signin" id="webauthn-form" action="" method="post">
{% csrf_token %}
<h3>{% trans "Welcome back!" %}</h3>
<p>
@@ -19,12 +19,12 @@
title="" type="password" required="" autofocus>
</div>
{% if jsondata %}
<div class="sr-only alert alert-danger" id="u2f-error">
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
<div class="sr-only alert alert-danger" id="webauthn-error">
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
</div>
<p><small>
<span class="fa fa-usb"></span>
{% trans "Alternatively, you can use your U2F device." %}
{% trans "Alternatively, you can use your WebAuthn device." %}
</small></p>
{% endif %}
<div class="form-group text-right">
@@ -37,14 +37,14 @@
</div>
{% if jsondata %}
<script type="text/json" id="u2f-login">
<script type="text/json" id="webauthn-login">
{{ jsondata|safe }}
</script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
{% endcompress %}
</form>
{% endblock %}

View File

@@ -62,8 +62,8 @@ urlpatterns = [
name='user.settings.2fa.regenemergency'),
url(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
name='user.settings.2fa.confirm.totp'),
url(r'^settings/2fa/u2f/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmU2FView.as_view(),
name='user.settings.2fa.confirm.u2f'),
url(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
name='user.settings.2fa.confirm.webauthn'),
url(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'),
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
@@ -106,6 +106,7 @@ urlpatterns = [
url(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
url(r'^$', dashboards.event_index, name='event.index'),
url(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
url(r'^live/$', event.EventLive.as_view(), name='event.live'),
url(r'^logs/$', event.EventLog.as_view(), name='event.log'),
url(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),

View File

@@ -1,7 +1,9 @@
import json
import logging
import time
from urllib.parse import quote
import webauthn
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
@@ -17,15 +19,13 @@ from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
from django_otp import match_token
from u2flib_server import u2f
from u2flib_server.jsapi import DeviceRegistration
from u2flib_server.utils import rand_bytes
from pretix.base.forms.auth import (
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
)
from pretix.base.models import TeamInvite, U2FDevice, User
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.base.services.mail import SendMailException
from pretix.helpers.webauthn import generate_challenge
logger = logging.getLogger(__name__)
@@ -302,7 +302,7 @@ class Recover(TemplateView):
def get_u2f_appid(request):
return '%s://%s' % ('https' if request.is_secure() else 'http', request.get_host())
return settings.SITE_URL
class Login2FAView(TemplateView):
@@ -333,15 +333,41 @@ class Login2FAView(TemplateView):
token = request.POST.get('token', '').strip().replace(' ', '')
valid = False
if '_u2f_challenge' in self.request.session and token.startswith('{'):
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
challenge = self.request.session.pop('_u2f_challenge')
if 'webauthn_challenge' in self.request.session and token.startswith('{'):
challenge = self.request.session['webauthn_challenge']
resp = json.loads(self.request.POST.get("token"))
try:
u2f.verify_authenticate(devices, challenge, token, [self.app_id])
valid = True
except Exception:
logger.exception('U2F login failed')
devices = [WebAuthnDevice.objects.get(user=self.user, credential_id=resp.get("id"))]
except WebAuthnDevice.DoesNotExist:
devices = U2FDevice.objects.filter(user=self.user)
for d in devices:
try:
wu = d.webauthnuser
if isinstance(d, U2FDevice):
# RP_ID needs to be appId for U2F devices, but we can't
# set it that way in U2FDevice.webauthnuser, since that
# breaks the frontend part.
wu.rp_id = settings.SITE_URL
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
wu,
resp,
challenge,
settings.SITE_URL,
uv_required=False # User Verification
)
sign_count = webauthn_assertion_response.verify()
except Exception:
logger.exception('U2F login failed')
else:
if isinstance(d, WebAuthnDevice):
d.sign_count = sign_count
d.save()
valid = True
break
else:
valid = match_token(self.user, token)
@@ -359,18 +385,25 @@ class Login2FAView(TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge']
challenge = generate_challenge(32)
self.request.session['webauthn_challenge'] = challenge
devices = [
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user)
] + [
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.user)
]
if devices:
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
self.request.session['_u2f_challenge'] = challenge.json
ctx['jsondata'] = challenge.json
else:
if '_u2f_challenge' in self.request.session:
del self.request.session['_u2f_challenge']
ctx['jsondata'] = None
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
devices,
challenge
)
ad = webauthn_assertion_options.assertion_dict
ad['extensions'] = {
'appid': get_u2f_appid(self.request)
}
ctx['jsondata'] = json.dumps(ad)
return ctx
def get(self, request, *args, **kwargs):

View File

@@ -8,6 +8,7 @@ from django.db.models import (
)
from django.db.models.functions import Coalesce, Greatest
from django.dispatch import receiver
from django.http import JsonResponse
from django.shortcuts import render
from django.template.loader import get_template
from django.urls import reverse
@@ -36,44 +37,46 @@ NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="
@receiver(signal=event_dashboard_widgets)
def base_widgets(sender, subevent=None, **kwargs):
prodc = Item.objects.filter(
event=sender, active=True,
).filter(
(Q(available_until__isnull=True) | Q(available_until__gte=now())) &
(Q(available_from__isnull=True) | Q(available_from__lte=now()))
).count()
def base_widgets(sender, subevent=None, lazy=False, **kwargs):
if not lazy:
prodc = Item.objects.filter(
event=sender, active=True,
).filter(
(Q(available_until__isnull=True) | Q(available_until__gte=now())) &
(Q(available_from__isnull=True) | Q(available_from__lte=now()))
).count()
if subevent:
opqs = OrderPosition.objects.filter(subevent=subevent)
else:
opqs = OrderPosition.objects
if subevent:
opqs = OrderPosition.objects.filter(subevent=subevent)
else:
opqs = OrderPosition.objects
tickc = opqs.filter(
order__event=sender, item__admission=True,
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
).count()
tickc = opqs.filter(
order__event=sender, item__admission=True,
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
).count()
paidc = opqs.filter(
order__event=sender, item__admission=True,
order__status=Order.STATUS_PAID,
).count()
paidc = opqs.filter(
order__event=sender, item__admission=True,
order__status=Order.STATUS_PAID,
).count()
if subevent:
rev = opqs.filter(
order__event=sender, order__status=Order.STATUS_PAID
).aggregate(
sum=Sum('price')
)['sum'] or Decimal('0.00')
else:
rev = Order.objects.filter(
event=sender,
status=Order.STATUS_PAID
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
if subevent:
rev = opqs.filter(
order__event=sender, order__status=Order.STATUS_PAID
).aggregate(
sum=Sum('price')
)['sum'] or Decimal('0.00')
else:
rev = Order.objects.filter(
event=sender,
status=Order.STATUS_PAID
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
return [
{
'content': NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
'lazy': 'attendees-ordered',
'display_size': 'small',
'priority': 100,
'url': reverse('control:event.orders', kwargs={
@@ -82,7 +85,8 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
'lazy': 'attendees-paid',
'display_size': 'small',
'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={
@@ -91,8 +95,9 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(
'content': None if lazy else NUM_WIDGET.format(
num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)),
'lazy': 'total-revenue',
'display_size': 'small',
'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={
@@ -101,7 +106,8 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': NUM_WIDGET.format(num=prodc, text=_('Active products')),
'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
'lazy': 'active-products',
'display_size': 'small',
'priority': 100,
'url': reverse('control:event.items', kwargs={
@@ -113,32 +119,36 @@ def base_widgets(sender, subevent=None, **kwargs):
@receiver(signal=event_dashboard_widgets)
def waitinglist_widgets(sender, subevent=None, **kwargs):
def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
widgets = []
wles = WaitingListEntry.objects.filter(event=sender, subevent=subevent, voucher__isnull=True)
if wles.count():
quota_cache = {}
itemvar_cache = {}
happy = 0
if not lazy:
quota_cache = {}
itemvar_cache = {}
happy = 0
for wle in wles:
if (wle.item, wle.variation) not in itemvar_cache:
itemvar_cache[(wle.item, wle.variation)] = (
wle.variation.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
)
row = itemvar_cache.get((wle.item, wle.variation))
if row[1] is None:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1])
happy += 1
elif row[1] > 0:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1] - 1)
happy += 1
for wle in wles:
if (wle.item, wle.variation) not in itemvar_cache:
itemvar_cache[(wle.item, wle.variation)] = (
wle.variation.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
)
row = itemvar_cache.get((wle.item, wle.variation))
if row[1] is None:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1])
happy += 1
elif row[1] > 0:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1] - 1)
happy += 1
widgets.append({
'content': NUM_WIDGET.format(num=str(happy), text=_('available to give to people on waiting list')),
'content': None if lazy else NUM_WIDGET.format(
num=str(happy), text=_('available to give to people on waiting list')
),
'lazy': 'waitinglist-avail',
'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={
'event': sender.slug,
@@ -146,7 +156,8 @@ def waitinglist_widgets(sender, subevent=None, **kwargs):
})
})
widgets.append({
'content': NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
'lazy': 'waitinglist-length',
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={
@@ -159,14 +170,18 @@ def waitinglist_widgets(sender, subevent=None, **kwargs):
@receiver(signal=event_dashboard_widgets)
def quota_widgets(sender, subevent=None, **kwargs):
def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
widgets = []
for q in sender.quotas.filter(subevent=subevent):
status, left = q.availability(allow_cache=True)
if not lazy:
status, left = q.availability(allow_cache=True)
widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=escape(q.name))),
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=escape(q.name))
),
'lazy': 'quota-{}'.format(q.pk),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.items.quotas.show', kwargs={
@@ -209,14 +224,18 @@ def shop_state_widget(sender, **kwargs):
@receiver(signal=event_dashboard_widgets)
def checkin_widget(sender, subevent=None, **kwargs):
def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
widgets = []
qs = sender.checkin_lists.filter(subevent=subevent)
qs = CheckinList.annotate_with_numbers(qs, sender)
if not lazy:
qs = CheckinList.annotate_with_numbers(qs, sender)
for cl in qs:
widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(cl.checkin_count, cl.position_count),
text=_('Checked in {list}').format(list=escape(cl.name))),
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(cl.checkin_count, cl.position_count),
text=_('Checked in {list}').format(list=escape(cl.name))
),
'lazy': 'checkin-{}'.format(cl.pk),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.checkinlists.show', kwargs={
@@ -263,7 +282,7 @@ def event_index(request, organizer, event):
pass
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
widgets.extend(result)
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
@@ -320,6 +339,22 @@ def event_index(request, organizer, event):
return resp
def event_index_widgets_lazy(request, organizer, event):
subevent = None
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
i = request.GET.get("subevent", "")
try:
subevent = request.event.subevents.get(pk=i)
except SubEvent.DoesNotExist:
pass
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return JsonResponse({'widgets': widgets})
def annotated_event_query(request):
active_orders = Order.objects.filter(
event=OuterRef('pk'),

View File

@@ -570,8 +570,11 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
kv = {
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company', 'total_with_currency',
'event', 'payment_info', 'url', 'invoice_name'],
'mail_text_order_placed_attendee': ['event', 'url', 'attendee_name'],
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
'mail_text_order_paid_attendee': ['event', 'url', 'attendee_name'],
'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_order_free_attendee': ['event', 'url', 'attendee_name'],
'mail_text_resend_link': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_resend_all_links': ['event', 'orders'],
'mail_text_order_changed': ['event', 'url', 'invoice_name', 'invoice_company'],
@@ -581,6 +584,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'mail_text_order_custom_mail': ['expire_date', 'event', 'code', 'date', 'url',
'invoice_name', 'invoice_company'],
'mail_text_download_reminder': ['event', 'url'],
'mail_text_download_reminder_attendee': ['attendee_name', 'event', 'url'],
'mail_text_order_placed_require_approval': ['total', 'currency', 'date', 'invoice_company',
'total_with_currency', 'event', 'url', 'invoice_name'],
'mail_text_order_approved': ['total', 'currency', 'date', 'invoice_company',
@@ -615,8 +619,9 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'code': '68CYU2H6ZTP3WLK5',
'invoice_name': _('John Doe'),
'invoice_company': _('Sample Corporation'),
'common': _('An individial text with a reason can be inserted here.'),
'common': _('An individual text with a reason can be inserted here.'),
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999'),
'attendee_name': _('John Doe'),
}
for k, v in self.request.event.meta_data.items():
d['meta_' + k] = v

View File

@@ -41,7 +41,7 @@ from pretix.base.models import (
from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund,
)
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.payment import PaymentException
from pretix.base.services import tickets
from pretix.base.services.export import export
@@ -983,7 +983,7 @@ class OrderCheckVATID(OrderView):
'specified.'))
return redirect(self.get_order_url())
if ia.vat_id[:2] != str(ia.country):
if ia.vat_id[:2] != cc_to_vat_prefix(str(ia.country)):
messages.error(self.request, _('Your VAT ID does not match the selected country.'))
return redirect(self.get_order_url())
@@ -1525,7 +1525,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
order.send_mail(
form.cleaned_data['subject'], email_template,
email_context, 'pretix.event.order.email.custom_sent',
self.request.user
self.request.user, auto_email=False
)
messages.success(self.request,
_('Your message has been queued and will be sent to {}.'.format(order.email)))

View File

@@ -343,7 +343,9 @@ class OrganizerCreate(CreateView):
return ret
def get_success_url(self) -> str:
return reverse('control:organizers')
return reverse('control:organizer', kwargs={
'organizer': self.object.slug,
})
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
@@ -655,7 +657,9 @@ class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
context_object_name = 'devices'
def get_queryset(self):
return self.request.organizer.devices.prefetch_related('limit_events')
return self.request.organizer.devices.prefetch_related(
'limit_events'
).order_by('-device_id')
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):

View File

@@ -2,8 +2,10 @@ import json
import logging
import mimetypes
from datetime import timedelta
from io import BytesIO
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.http import (
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
@@ -14,6 +16,8 @@ from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from PyPDF2 import PdfFileWriter
from reportlab.lib.units import mm
from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
@@ -117,6 +121,33 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
self.request.event.settings.set(self.get_background_settings_key(), 'file://' + newname)
def post(self, request, *args, **kwargs):
if "emptybackground" in request.POST:
p = PdfFileWriter()
p.addBlankPage(
width=float(request.POST.get('width')) * mm,
height=float(request.POST.get('height')) * mm,
)
buffer = BytesIO()
p.write(buffer)
buffer.seek(0)
c = CachedFile()
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.type = 'application/pdf'
c.save()
c.file.save('empty.pdf', ContentFile(buffer.read()))
c.refresh_from_db()
return JsonResponse({
"status": "ok",
"id": c.id,
"url": reverse('control:pdf.background', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'filename': str(c.id)
})
})
if "background" in request.FILES:
error, fileobj = self.process_upload()
if error:

View File

@@ -1,12 +1,16 @@
import base64
import json
import logging
import os
import time
from collections import defaultdict
from urllib.parse import quote
from urllib.parse import quote, urlparse
import webauthn
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
@@ -18,12 +22,11 @@ from django.views import View
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from u2flib_server import u2f
from u2flib_server.jsapi import DeviceRegistration
from u2flib_server.utils import rand_bytes
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.models import Event, NotificationSetting, U2FDevice, User
from pretix.base.models import (
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
)
from pretix.base.models.auth import StaffSession
from pretix.base.notifications import get_all_notification_types
from pretix.control.forms.users import StaffSessionForm
@@ -31,8 +34,9 @@ from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
)
from pretix.control.views.auth import get_u2f_appid
from pretix.helpers.webauthn import generate_challenge, generate_ukey
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
logger = logging.getLogger(__name__)
@@ -49,23 +53,45 @@ class RecentAuthenticationRequiredMixin:
class ReauthView(TemplateView):
template_name = 'pretixcontrol/user/reauth.html'
@property
def app_id(self):
return get_u2f_appid(self.request)
def post(self, request, *args, **kwargs):
password = request.POST.get("password", "")
valid = False
if '_u2f_challenge' in self.request.session and password.startswith('{'):
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
challenge = self.request.session.pop('_u2f_challenge')
if 'webauthn_challenge' in self.request.session and password.startswith('{'):
challenge = self.request.session['webauthn_challenge']
resp = json.loads(password)
try:
u2f.verify_authenticate(devices, challenge, password, [self.app_id])
valid = True
except Exception:
logger.exception('U2F login failed')
devices = [WebAuthnDevice.objects.get(user=self.request.user, credential_id=resp.get("id"))]
except WebAuthnDevice.DoesNotExist:
devices = U2FDevice.objects.filter(user=self.request.user)
for d in devices:
try:
wu = d.webauthnuser
if isinstance(d, U2FDevice):
# RP_ID needs to be appId for U2F devices, but we can't
# set it that way in U2FDevice.webauthnuser, since that
# breaks the frontend part.
wu.rp_id = settings.SITE_URL
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
wu,
resp,
challenge,
settings.SITE_URL,
uv_required=False # User Verification
)
sign_count = webauthn_assertion_response.verify()
except Exception:
logger.exception('U2F login failed')
else:
if isinstance(d, WebAuthnDevice):
d.sign_count = sign_count
d.save()
valid = True
break
valid = valid or request.user.check_password(password)
@@ -82,18 +108,25 @@ class ReauthView(TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge']
challenge = generate_challenge(32)
self.request.session['webauthn_challenge'] = challenge
devices = [
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
] + [
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
]
if devices:
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
self.request.session['_u2f_challenge'] = challenge.json
ctx['jsondata'] = challenge.json
else:
if '_u2f_challenge' in self.request.session:
del self.request.session['_u2f_challenge']
ctx['jsondata'] = None
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
devices,
challenge
)
ad = webauthn_assertion_options.assertion_dict
ad['extensions'] = {
'appid': get_u2f_appid(self.request)
}
ctx['jsondata'] = json.dumps(ad)
return ctx
@@ -149,12 +182,29 @@ class UserSettings(UpdateView):
return reverse('control:user.settings')
class UserHistoryView(TemplateView):
class UserHistoryView(ListView):
template_name = 'pretixcontrol/user/history.html'
model = LogEntry
context_object_name = 'logs'
paginate_by = 20
def get_queryset(self):
qs = LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(User),
object_id=self.request.user.pk
).select_related(
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['user'] = self.request.user
ctx = super().get_context_data()
class FakeClass:
def top_logentries(self):
return ctx['logs']
ctx['fakeobj'] = FakeClass()
return ctx
@@ -180,6 +230,8 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
elif dt == WebAuthnDevice:
obj.devicetype = 'webauthn'
ctx['devices'] += objs
return ctx
@@ -192,11 +244,12 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
def form_valid(self, form):
if form.cleaned_data['devicetype'] == 'totp':
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
elif form.cleaned_data['devicetype'] == 'u2f':
elif form.cleaned_data['devicetype'] == 'webauthn':
if not self.request.is_secure():
messages.error(self.request, _('U2F devices are only available if pretix is served via HTTPS.'))
messages.error(self.request,
_('Security devices are only available if pretix is served via HTTPS.'))
return self.get(self.request, self.args, self.kwargs)
dev = U2FDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
dev = WebAuthnDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
'device': dev.pk
}))
@@ -213,6 +266,8 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
def device(self):
if self.kwargs['devicetype'] == 'totp':
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'webauthn':
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'u2f':
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
@@ -242,35 +297,94 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
return redirect(reverse('control:user.settings.2fa'))
class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
@property
def app_id(self):
return get_u2f_appid(self.request)
class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_webauthn.html'
@cached_property
def device(self):
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['device'] = self.device
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
enroll = u2f.start_register(self.app_id, devices)
self.request.session['_u2f_enroll'] = enroll.json
ctx['jsondata'] = enroll.json
if 'webauthn_register_ukey' in self.request.session:
del self.request.session['webauthn_register_ukey']
if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge']
challenge = generate_challenge(32)
ukey = generate_ukey()
self.request.session['webauthn_challenge'] = challenge
self.request.session['webauthn_register_ukey'] = ukey
make_credential_options = webauthn.WebAuthnMakeCredentialOptions(
challenge,
urlparse(settings.SITE_URL).netloc,
urlparse(settings.SITE_URL).netloc,
ukey,
self.request.user.email,
str(self.request.user),
settings.SITE_URL
)
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict)
return ctx
def post(self, request, *args, **kwargs):
try:
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
request.POST.get('token'),
[self.app_id])
self.device.json_data = binding.json
challenge = self.request.session['webauthn_challenge']
ukey = self.request.session['webauthn_register_ukey']
resp = json.loads(self.request.POST.get("token"))
trust_anchor_dir = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'../../static/webauthn_trusted_attestation_roots' # currently does not exist
))
# We currently do not check attestation certificates, since there's no real risk
# and we do not have any policies specifying what devices can be used. (Also, we
# didn't get it to work.)
# Read more: https://fidoalliance.org/fido-technotes-the-truth-about-attestation/
trusted_attestation_cert_required = False
self_attestation_permitted = True
none_attestation_permitted = True
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse(
urlparse(settings.SITE_URL).netloc,
settings.SITE_URL,
resp,
challenge,
trust_anchor_dir,
trusted_attestation_cert_required,
self_attestation_permitted,
none_attestation_permitted,
uv_required=False
)
webauthn_credential = webauthn_registration_response.verify()
# Check that the credentialId is not yet registered to any other user.
# If registration is requested for a credential that is already registered
# to a different user, the Relying Party SHOULD fail this registration
# ceremony, or it MAY decide to accept the registration, e.g. while deleting
# the older registration.
credential_id_exists = WebAuthnDevice.objects.filter(
credential_id=webauthn_credential.credential_id
).first()
if credential_id_exists:
messages.error(request, _('This security device is already registered.'))
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
'device': self.device.pk
}))
webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8")
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
self.device.credential_id = webauthn_credential.credential_id
self.device.ukey = ukey
self.device.pub_key = webauthn_credential.public_key
self.device.sign_count = webauthn_credential.sign_count
self.device.rp_id = urlparse(settings.SITE_URL).netloc
self.device.icon_url = settings.SITE_URL
self.device.confirmed = True
self.device.save()
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
@@ -300,8 +414,8 @@ class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateVie
return redirect(reverse('control:user.settings.2fa'))
except Exception:
messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('U2F registration failed')
return redirect(reverse('control:user.settings.2fa.confirm.u2f', kwargs={
logger.exception('WebAuthn registration failed')
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
'device': self.device.pk
}))

View File

@@ -0,0 +1,24 @@
import random
import string
def generate_challenge(challenge_len):
return ''.join([
random.SystemRandom().choice(string.ascii_letters + string.digits)
for i in range(challenge_len)
])
def generate_ukey():
"""
Its value's id member is required, and contains an identifier
for the account, specified by the Relying Party. This is not meant
to be displayed to the user, but is used by the Relying Party to
control the number of credentials - an authenticator will never
contain more than one credential for a given Relying Party under
the same id.
A unique identifier for the entity. For a relying party entity,
sets the RP ID. For a user account entity, this will be an
arbitrary string specified by the relying party.
"""
return generate_challenge(20)

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -162,12 +162,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -196,19 +197,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -161,12 +161,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -195,19 +196,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -161,12 +161,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -195,19 +196,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,9 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-09-05 18:00+0000\n"
"Last-Translator: Ture Gjørup <ture@ignatz.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"da/>\n"
"Language: da\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2.19.1\n"
"X-Generator: Weblate 3.5.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -48,18 +48,16 @@ msgstr "Kontakter Stripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
msgid "Total"
msgstr ""
msgstr "I alt"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
msgid "Confirming your payment …"
msgstr ""
msgstr "Bekræfter din betaling …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
#, fuzzy
#| msgid "Contacting Stripe …"
msgid "Contacting your bank …"
msgstr "Kontakter Stripe …"
msgstr "Kontakter din bank …"
#: pretix/static/pretixbase/js/asynctask.js:39
#: pretix/static/pretixbase/js/asynctask.js:105
@@ -72,11 +70,6 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:45
#: pretix/static/pretixbase/js/asynctask.js:111
#, fuzzy
#| msgid ""
#| "Your request has been queued on the server and will now be processed. If "
#| "this takes longer than two minutes, please contact us or go back in your "
#| "browser and try again."
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
@@ -145,7 +138,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:45
msgid "Check-in QR"
msgstr ""
msgstr "Check-in QR"
#: pretix/static/pretixcontrol/js/ui/editor.js:249
msgid "The PDF background file could not be loaded for the following reason:"
@@ -165,7 +158,7 @@ msgstr "QR-kode-område"
#: pretix/static/pretixcontrol/js/ui/editor.js:428
msgid "Powered by pretix"
msgstr ""
msgstr "Drevet af pretix"
#: pretix/static/pretixcontrol/js/ui/editor.js:430
msgid "Object"
@@ -179,15 +172,16 @@ msgstr "Billetdesign"
msgid "Saving failed."
msgstr "Gem fejlede."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr "Fejl under upload af pdf. Prøv venligt igen."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
msgstr "Fejl under upload af pdf. Prøv venligt igen."
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Der er sket en fejl."
@@ -214,19 +208,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-07-29 08:36+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -176,16 +176,17 @@ msgstr "Ticket-Design"
msgid "Saving failed."
msgstr "Speichern fehlgeschlagen."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Ein Fehler ist aufgetreten."
@@ -215,19 +216,19 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Klicken zum Schließen"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-07-29 08:36+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -175,16 +175,17 @@ msgstr "Ticket-Design"
msgid "Saving failed."
msgstr "Speichern fehlgeschlagen."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Ein Fehler ist aufgetreten."
@@ -214,19 +215,19 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wähle eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Klicken zum Schließen"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -162,12 +162,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -196,19 +197,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-06-04 16:00+0000\n"
"Last-Translator: ThanosTeste <testebasisth@unisystems.eu>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -179,16 +179,17 @@ msgstr "Σχεδιασμός εισιτηρίων"
msgid "Saving failed."
msgstr "Η αποθήκευση απέτυχε."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Θέλετε πραγματικά να αφήσετε τον επεξεργαστή χωρίς να αποθηκεύσετε τις "
"αλλαγές σας;"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Παρουσιάστηκε σφάλμα."
@@ -220,19 +221,19 @@ msgstr ""
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
"σκούρα σκιά."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Ολα"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-03-31 08:00+0000\n"
"Last-Translator: oocf <oswaldocerna@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -178,16 +178,17 @@ msgstr "Diseño del ticket"
msgid "Saving failed."
msgstr "El guardado falló."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "¿Realmente desea salir del editor sin haber guardado sus cambios?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Ha habido un error mientras se cargaba el archivo PDF, por favor, intente de "
"nuevo."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "¿Realmente desea salir del editor sin haber guardado sus cambios?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Ha ocurrido un error."
@@ -218,19 +219,19 @@ msgstr ""
"Tu color tiene mal contraste para un texto con fondo blanco, por favor "
"escoge un tono más oscuro."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Click para cerrar"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-08-01 18:41+0000\n"
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -183,16 +183,17 @@ msgstr "Conception des billets"
msgid "Saving failed."
msgstr "L'enregistrement a échoué."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Voulez-vous vraiment quitter l'éditeur sans sauvegarder vos modifications ?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
"Erreur lors du téléchargement de votre fichier PDF, veuillez réessayer."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Voulez-vous vraiment quitter l'éditeur sans sauvegarder vos modifications ?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Une erreur s'est produite."
@@ -219,19 +220,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Tous"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Aucun"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Utiliser un nom différent en interne"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-01-02 08:20+0000\n"
"Last-Translator: amefad <fame@libero.it>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -178,12 +178,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -212,19 +213,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -162,12 +162,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -196,19 +197,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-08-03 22:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -173,14 +173,15 @@ msgstr "Ticketontwerp"
msgid "Saving failed."
msgstr "Opslaan mislukt."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Wilt u de editor verlaten zonder uw wijzigingen op te slaan?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr "Probleem bij het uploaden van het PDF-bestand, probeer het opnieuw."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Wilt u de editor verlaten zonder uw wijzigingen op te slaan?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Er is een fout opgetreden."
@@ -210,19 +211,19 @@ msgstr ""
"Uw kleur heeft een slecht contrast voor tekst op een witte achtergrond, kies "
"een donkerdere kleur."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Geen"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Gebruik intern een andere naam"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Klik om te sluiten"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -161,12 +161,13 @@ msgstr ""
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:18
@@ -195,19 +196,19 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-08-03 22:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
@@ -175,14 +175,15 @@ msgstr "Kaartjesontwerp"
msgid "Saving failed."
msgstr "Opslaan mislukt."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Wil je de editor verlaten zonder je wijzigingen op te slaan?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr "Probleem bij het uploaden van het PDF-bestand, probeer het opnieuw."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Wil je de editor verlaten zonder je wijzigingen op te slaan?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Er is iets misgegaan."
@@ -212,19 +213,19 @@ msgstr ""
"Je kleur heeft een slecht contrast voor tekst op een witte achtergrond, kies "
"een donkerdere kleur."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Geen"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Gebruik intern een andere naam"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Klik om te sluiten"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-08 07:55+0000\n"
"POT-Creation-Date: 2019-09-10 08:00+0000\n"
"PO-Revision-Date: 2019-03-15 11:19+0000\n"
"Last-Translator: Serge Bazanski <q3k@hackerspace.pl>\n"
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -178,14 +178,15 @@ msgstr "Projekt biletu"
msgid "Saving failed."
msgstr "Błąd zapisu."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Czy na pewno opuścić edytor bez zapisania zmian?"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
#: pretix/static/pretixcontrol/js/ui/editor.js:736
#: pretix/static/pretixcontrol/js/ui/editor.js:774
msgid "Error while uploading your PDF file, please try again."
msgstr "Błąd uploadu pliku PDF, prosimy spróbować ponownie."
#: pretix/static/pretixcontrol/js/ui/editor.js:759
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Czy na pewno opuścić edytor bez zapisania zmian?"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Wystąpił błąd."
@@ -215,19 +216,19 @@ msgstr ""
"Wybrany kolor ma za słaby kontrast dla tekstu na białym tle, prosimy wybrać "
"ciemniejszy odcień."
#: pretix/static/pretixcontrol/js/ui/main.js:319
#: pretix/static/pretixcontrol/js/ui/main.js:355
msgid "All"
msgstr "Zaznacz wszystko"
#: pretix/static/pretixcontrol/js/ui/main.js:320
#: pretix/static/pretixcontrol/js/ui/main.js:356
msgid "None"
msgstr "Odznacz wszystko"
#: pretix/static/pretixcontrol/js/ui/main.js:609
#: pretix/static/pretixcontrol/js/ui/main.js:645
msgid "Use a different name internally"
msgstr "Użyj innej nazwy wewnętrznie"
#: pretix/static/pretixcontrol/js/ui/main.js:666
#: pretix/static/pretixcontrol/js/ui/main.js:702
msgid "Click to close"
msgstr "Zamknij"

File diff suppressed because it is too large Load Diff

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