Compare commits

...

100 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I'm using the hosted pretix version. 

Test for frontpage text: 

````
Test 

* test1
* test2
````

Before (screenshot): 

---

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

----


After (screenshot): 

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

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

View File

@@ -125,6 +125,23 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
Database replica settings
-------------------------
If you use a replicated database setup, pretix expects that the default database connection always points to the primary database node.
Routing read queries to a replica on database layer is **strongly** discouraged since this can lead to inaccurate such as more tickets
being sold than are actually available.
However, pretix can still make use of a database replica to keep some expensive queries with that can tolerate some latency from your
primary database, such as backend search queries. The ``replica`` configuration section can have the same settings as the ``database``
section (except for the ``backend`` setting) and will default back to the ``database`` settings for all values that are not given. This
way, you just need to specify the settings that are different for the replica.
Example::
[replica]
host=192.168.0.2
URLs
----

View File

@@ -565,7 +565,10 @@ Order position endpoints
Content-Type: application/json
{
"status": "ok"
"status": "ok",
"position": {
}
}
**Example response with required questions**:
@@ -576,7 +579,10 @@ Order position endpoints
Content-Type: text/json
{
"status": "incomplete"
"status": "incomplete",
"position": {
},
"questions": [
{
"id": 1,
@@ -621,6 +627,9 @@ Order position endpoints
{
"status": "error",
"reason": "unpaid",
"position": {
}
}
Possible error reasons:

View File

@@ -15,6 +15,7 @@ name multi-lingual string The event's ful
slug string A short form of the name, used e.g. in URLs.
live boolean If ``true``, the event ticket shop is publicly
available.
testmode boolean If ``true``, the ticket shop is in test mode.
currency string The currency this event is handled in.
date_from datetime The event's start date
date_to datetime The event's end date (or ``null``)
@@ -45,6 +46,10 @@ plugins list A list of packa
Filters have been added to the list of events.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added.
Endpoints
---------
@@ -79,6 +84,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -137,6 +143,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -183,6 +190,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -211,6 +219,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -259,6 +268,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -287,6 +297,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -347,6 +358,7 @@ Endpoints
"name": {"en": "Sample Conference"},
"slug": "sampleconf",
"live": false,
"testmode": false,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,

View File

@@ -26,6 +26,8 @@ status string Order status, o
* ``p`` paid
* ``e`` expired
* ``c`` canceled
testmode boolean If ``True``, this order was created when the event was in
test mode. Only orders in test mode can be deleted.
secret string The secret contained in the link sent to the customer
email string The customer email address
locale string The locale used for communication with this customer
@@ -131,6 +133,10 @@ last_modified datetime Last modificati
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5:
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. _order-position-resource:
Order position resource
@@ -272,6 +278,7 @@ List of all orders
{
"code": "ABC12",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
@@ -370,11 +377,14 @@ List of all orders
``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.
:query string email: Only return orders created with the given email address
:query string locale: Only return orders with the given customer locale
:query datetime modified_since: Only return orders that have changed since the given date
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -409,6 +419,7 @@ Fetching individual orders
{
"code": "ABC12",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"email": "tester@example.org",
"locale": "en",
@@ -552,6 +563,37 @@ Order ticket download
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Deleting orders
---------------
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
Deletes an order. Works only if the order has ``testmode`` set to ``true``.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
Content-Type: application/json
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
:statuscode 404: The requested order does not exist.
Creating orders
---------------
@@ -606,6 +648,7 @@ Creating orders
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
**not** be sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and
then call the ``mark_paid`` API method.
* ``testmode`` (optional) Defaults to ``false``
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas that become free by this operation will be credited to your order
creation.

View File

@@ -114,6 +114,8 @@ The provider class
.. autoattribute:: is_meta
.. autoattribute:: test_mode_message
Additional views
----------------

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

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

View File

@@ -111,6 +111,7 @@ submodule
subpath
Symfony
systemd
testmode
testutils
timestamp
tuples

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
@@ -47,7 +48,7 @@ class EventSerializer(I18nAwareModelSerializer):
class Meta:
model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from',
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
@@ -108,7 +109,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
plugins = validated_data.pop('plugins', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
event = super().create(validated_data)
# Meta data
@@ -122,6 +123,7 @@ class EventSerializer(I18nAwareModelSerializer):
# Plugins
if plugins is not None:
event.set_active_plugins(plugins)
event.save(update_fields=['plugins'])
return event

View File

@@ -12,6 +12,7 @@ from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
@@ -140,20 +141,21 @@ class PdfDataSerializer(serializers.Field):
res = {}
ev = instance.subevent or instance.order.event
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list.
with language(instance.order.locale):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list.
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
return res
@@ -229,7 +231,7 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
@@ -411,7 +413,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
def validate_payment_provider(self, pp):

View File

@@ -16,7 +16,9 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
from pretix.base.models import (
Checkin, CheckinList, Event, Order, OrderPosition,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
@@ -201,12 +203,39 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
).prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
).select_related('item', 'variation', 'order', 'addon_to')
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
@@ -251,6 +280,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -259,10 +290,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
return Response({
'status': 'error',
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self):

View File

@@ -16,7 +16,7 @@ from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
@@ -26,8 +26,8 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Invoice, Order, OrderPayment,
OrderPosition, OrderRefund, Quota, TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
@@ -51,10 +51,10 @@ class OrderFilter(FilterSet):
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'require_approval']
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -83,6 +83,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
)
)
@@ -377,6 +378,13 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def perform_create(self, serializer):
serializer.save()
def perform_destroy(self, instance):
if not instance.testmode:
raise PermissionDenied('Only test mode orders can be deleted.')
with transaction.atomic():
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
@@ -391,6 +399,7 @@ class OrderPositionFilter(FilterSet):
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
@@ -435,11 +444,33 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
}
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
qs = OrderPosition.objects.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
)
else:
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)

View File

@@ -113,7 +113,7 @@ def register_default_webhook_events(sender, **kwargs):
_('New order placed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.placed.required_approval',
'pretix.event.order.placed.require_approval',
_('New order requires approval'),
),
ParametrizedOrderWebhookEvent(

View File

@@ -27,7 +27,7 @@ class CustomSMTPBackend(EmailBackend):
if code != 250:
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
(code, resp) = self.connection.rcpt('test@example.com')
(code, resp) = self.connection.rcpt('testdummy@pretix.eu')
if (code != 250) and (code != 251):
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)

View File

@@ -209,7 +209,7 @@ class MultiSheetListExporter(ListExporter):
sheet, f = form_data.get('_format').split(':')
if f == 'default':
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
elif f == 'csv-excel':
elif f == 'excel':
return self._render_sheet_csv(form_data, sheet, dialect='excel')
elif f == 'semicolon':
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';')

View File

@@ -559,6 +559,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Tax value'),
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
@@ -598,6 +599,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
l.tax_value,
l.tax_rate,
l.tax_name,
date_format(l.event_date_from, "SHORT_DATE_FORMAT") if l.event_date_from else "",
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,

View File

@@ -1,9 +1,11 @@
import logging
import i18nfield.forms
from django import forms
from django.forms.models import ModelFormMetaclass
from django.utils import six
from django.utils.crypto import get_random_string
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
from pretix.base.models import Event
@@ -71,3 +73,29 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
# TODO: make sure pub is always correct
return 'pub/' + fname
class PrefixForm(forms.Form):
prefix = forms.CharField(widget=forms.HiddenInput)
class SafeSessionWizardView(SessionWizardView):
def get_prefix(self, request, *args, **kwargs):
if hasattr(request, '_session_wizard_prefix'):
return request._session_wizard_prefix
prefix_form = PrefixForm(self.request.POST, prefix=super().get_prefix(request, *args, **kwargs))
if not prefix_form.is_valid():
request._session_wizard_prefix = get_random_string(length=24)
else:
request._session_wizard_prefix = prefix_form.cleaned_data['prefix']
return request._session_wizard_prefix
def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
context['wizard']['prefix_form'] = PrefixForm(
prefix=super().get_prefix(self.request),
initial={
'prefix': self.get_prefix(self.request)
}
)
return context

View File

@@ -90,6 +90,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control splitdatetimepart')
time_attrs.setdefault('class', 'form-control splitdatetimepart')
date_attrs.setdefault('autocomplete', 'off')
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'

View File

@@ -9,7 +9,7 @@ import vat_moss.exchange_rates
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext
from django.utils.translation import pgettext, ugettext
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
@@ -267,6 +267,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.saveState()
canvas.setFont(self.font_regular, 8)
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
canvas.restoreState()
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())

View File

@@ -129,13 +129,22 @@ def get_language_from_request(request: HttpRequest) -> str:
if _supported is None:
_supported = OrderedDict(settings.LANGUAGES)
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
if request.path.startswith(get_script_prefix() + 'control'):
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
else:
return (
get_language_from_session_or_cookie(request)
or get_language_from_user_settings(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
def _parse_csp(header):

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,13 @@ class LoggingMixin:
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']
for sensitivekey in sensitivekeys:
for k, v in data.items():
if (sensitivekey in k) and v:
data[k] = "********"
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
elif data:
raise TypeError("You should only supply dictionaries as log data.")

View File

@@ -242,6 +242,8 @@ class Event(EventMixin, LoggedModel):
:param organizer: The organizer this event belongs to
:type organizer: Organizer
:param testmode: This event is in test mode
:type testmode: bool
:param name: This event's full title
:type name: str
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
@@ -271,6 +273,7 @@ class Event(EventMixin, LoggedModel):
settings_namespace = 'event'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
testmode = models.BooleanField(default=False)
name = I18nCharField(
max_length=200,
verbose_name=_("Event name"),
@@ -321,7 +324,7 @@ class Event(EventMixin, LoggedModel):
verbose_name=_("Location"),
)
plugins = models.TextField(
null=True, blank=True,
null=False, blank=True,
verbose_name=_("Plugins"),
)
comment = models.TextField(

View File

@@ -118,8 +118,8 @@ class Invoice(models.Model):
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
str(self.invoice_from_country),
pgettext("invoice", "VAT-ID: %s" % self.invoice_from_vat_id) if self.invoice_from_vat_id else "",
pgettext("invoice", "Tax ID: %s" % self.invoice_from_tax_id) if self.invoice_from_tax_id else "",
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
@@ -150,6 +150,8 @@ class Invoice(models.Model):
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if not self.invoice_no:
if self.order.testmode:
self.prefix += 'TEST-'
for i in range(10):
if self.event.settings.get('invoice_numbers_consecutive'):
self.invoice_no = self._get_numeric_invoice_number()
@@ -212,6 +214,10 @@ class InvoiceLine(models.Model):
:type tax_rate: decimal.Decimal
:param tax_name: The name of the applied tax rate
:type tax_name: str
:param subevent: The subevent this line refers to
:type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created
:type event_date_from: datetime
"""
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0)
@@ -220,6 +226,8 @@ class InvoiceLine(models.Model):
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
tax_name = models.CharField(max_length=190)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
@property
def net_value(self):
@@ -227,3 +235,6 @@ class InvoiceLine(models.Model):
class Meta:
ordering = ('position', 'pk')
def __str__(self):
return 'Line {} of invoice {}'.format(self.position, self.invoice)

View File

@@ -76,6 +76,8 @@ class Order(LockModel, LoggedModel):
:type event: Event
:param email: The email of the person who ordered this
:type email: str
:param testmode: Whether this is a test mode order
:type testmode: bool
:param locale: The locale of this order
:type locale: str
:param secret: A secret string that is required to modify the order
@@ -121,6 +123,7 @@ class Order(LockModel, LoggedModel):
verbose_name=_("Status"),
db_index=True
)
testmode = models.BooleanField(default=False)
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
@@ -185,6 +188,23 @@ class Order(LockModel, LoggedModel):
def __str__(self):
return self.full_code
def gracefully_delete(self, user=None, auth=None):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.event.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
self.delete()
@property
def fees(self):
"""
@@ -490,6 +510,10 @@ class Order(LockModel, LoggedModel):
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
if self.testmode:
# Subtle way to recognize test orders while debugging: They all contain a 0 at the second place,
# even though zeros are not used outside test mode.
code = code[0] + "0" + code[2:]
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
self.code = code
return
@@ -661,6 +685,9 @@ class Order(LockModel, LoggedModel):
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
try:
@@ -1037,6 +1064,9 @@ class OrderPayment(models.Model):
class Meta:
ordering = ('local_id',)
def __str__(self):
return self.full_id
@property
def info_data(self):
"""
@@ -1098,9 +1128,23 @@ class OrderPayment(models.Model):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
self.state = self.PAYMENT_STATE_CONFIRMED
self.payment_date = now()
self.save()
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
# Race condition detected, this payment is already confirmed
return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
locked_instance.payment_date = now()
locked_instance.info = self.info # required for backwards compatibility
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
# Do a cheap manual "refresh from db" on non-complex fields
for field in self._meta.concrete_fields:
if not field.is_relation:
setattr(self, field.attname, getattr(locked_instance, field.attname))
self.refresh_from_db()
self.order.log_action('pretix.event.order.payment.confirmed', {
'local_id': self.local_id,
@@ -1329,6 +1373,9 @@ class OrderRefund(models.Model):
class Meta:
ordering = ('local_id',)
def __str__(self):
return self.full_id
@property
def info_data(self):
"""

View File

@@ -177,6 +177,7 @@ class ParametrizedOrderNotificationType(NotificationType):
n.add_attribute(_('Event'), order.event.name)
n.add_attribute(_('Order code'), order.code)
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
n.add_attribute(_('Pending amount'), money_filter(order.pending_sum, logentry.event.currency))
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
n.add_attribute(_('Order status'), order.get_status_display())
n.add_attribute(_('Order positions'), str(order.positions.count()))

View File

@@ -88,6 +88,18 @@ class BasePaymentProvider:
"""
return self.settings.get('_enabled', as_type=bool)
@property
def test_mode_message(self) -> str:
"""
If this property is set to a string, this will be displayed when this payment provider is selected
while the event is in test mode. You should use it to explain to your user how your plugin behaves,
e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.
If you do not set this (or, return ``None``), pretix will show a default message warning the user
that this plugin does not support test mode payments.
"""
return None
def calculate_fee(self, price: Decimal) -> Decimal:
"""
Calculate the fee for this payment provider which will be added to
@@ -713,6 +725,11 @@ class ManualPayment(BasePaymentProvider):
identifier = 'manual'
verbose_name = _('Manual payment')
@property
def test_mode_message(self):
return _('In test mode, you can just manually mark this order as paid in the backend after it has been '
'created.')
@property
def is_implicit(self):
return 'pretix.plugins.manualpayment' not in self.event.plugins
@@ -788,9 +805,9 @@ class ManualPayment(BasePaymentProvider):
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
return msg
def order_pending_render(self, request, order) -> str:
def payment_pending_render(self, request, payment) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(order))
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order))
)

View File

@@ -371,7 +371,7 @@ class CartManager:
# could cancel each other out quota-wise). However, we are not taking this performance
# penalty for now as there is currently no outside interface that would allow building
# such a transaction.
for cp in self.positions.all():
for cp in self.positions.filter(addon_to__isnull=True):
self._operations.append(self.RemoveOperation(position=cp))
def set_addons(self, addons):
@@ -653,6 +653,7 @@ class CartManager:
op.position.price = op.price.gross
op.position.save()
elif available_count == 0:
op.position.addons.all().delete()
op.position.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
@@ -669,15 +670,15 @@ class CartManager:
self._check_max_cart_size()
self._calculate_expiry()
with self.event.lock() as now_dt:
with transaction.atomic():
self.now_dt = now_dt
self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err
if err:
raise CartError(err)
# with self.event.lock() as now_dt:
with transaction.atomic():
self.now_dt = now()
self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err
if err:
raise CartError(err)
def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress):

View File

@@ -142,6 +142,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
InvoiceLine.objects.create(
position=i, invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)

View File

@@ -79,6 +79,10 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
headers = headers or {}
with language(locale):
if isinstance(context, dict) and event:
for k, v in event.meta_data.items():
context['meta_' + k] = v
if isinstance(context, dict) and order:
try:
context.update({
@@ -126,6 +130,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += "\r\n\r\n-- \r\n"
if order:
if order.testmode:
subject = "[TESTMODE] " + subject
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)

View File

@@ -10,6 +10,7 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
from django.db.models import F, Max, Q, Sum
from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
@@ -255,7 +256,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
if send_mail:
try:
@@ -323,7 +324,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
with order.event.lock():
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True
position.save(update_fields=['canceled'])
for fee in order.fees.all():
@@ -354,7 +355,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
@@ -532,6 +533,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
datetime=now_dt,
locale=locale,
total=total,
testmode=event.testmode,
meta_info=json.dumps(meta_info or {}),
require_approval=any(p.item.require_approval for p in positions),
sales_channel=sales_channel
@@ -595,16 +597,16 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
except InvoiceAddress.DoesNotExist:
pass
with event.lock() as now_dt:
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation', 'subevent'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr)
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
# with event.lock() as now_dt:
now_dt = now()
positions = list(CartPosition.objects.filter(id__in=position_ids).select_related('item', 'variation', 'subevent'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr)
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -814,12 +816,17 @@ class OrderChangeManager:
self.notify = notify
self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation], keep_price=False):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if keep_price:
price = TaxedPrice(gross=position.price, net=position.price - position.tax_value,
tax=position.tax_value, rate=position.tax_rate,
name=position.tax_rule.name if position.tax_rule else None)
else:
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
@@ -956,7 +963,7 @@ class OrderChangeManager:
)
self.order.save()
elif self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and self._totaldiff < 0:
if self.order.pending_sum <= Decimal('0.00'):
if self.order.pending_sum <= Decimal('0.00') and not self.order.require_approval:
self.order.status = Order.STATUS_PAID
self.order.save()
elif self.open_payment:
@@ -976,7 +983,7 @@ class OrderChangeManager:
}, user=self.user, auth=self.auth)
def _check_paid_to_free(self):
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)):
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval:
# if the order becomes free, mark it paid using the 'free' provider
# this could happen if positions have been made cheaper or removed (_totaldiff < 0)
# or positions got split off to a new order (split_order with positive total)
@@ -991,7 +998,7 @@ class OrderChangeManager:
except Quota.QuotaExceededException:
raise OrderError(self.error_messages['paid_to_free_exceeded'])
if self.split_order and self.split_order.total == 0:
if self.split_order and self.split_order.total == 0 and not self.split_order.require_approval:
p = self.split_order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider='free',
@@ -1067,7 +1074,7 @@ class OrderChangeManager:
})
opa.canceled = True
if opa.voucher:
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=opa.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
opa.save(update_fields=['canceled'])
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': op.position.pk,
@@ -1079,7 +1086,7 @@ class OrderChangeManager:
})
op.position.canceled = True
if op.position.voucher:
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=F('redeemed') - 1)
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
op.position.save(update_fields=['canceled'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
@@ -1119,6 +1126,7 @@ class OrderChangeManager:
split_order.code = None
split_order.datetime = now()
split_order.secret = generate_secret()
split_order.require_approval = self.order.require_approval and any(p.item.require_approval for p in split_positions)
split_order.save()
split_order.log_action('pretix.event.order.changed.split_from', user=self.user, auth=self.auth, data={
'original_order': self.order.code
@@ -1300,7 +1308,7 @@ class OrderChangeManager:
except SendMailException:
logger.exception('Order changed email could not be sent')
def commit(self):
def commit(self, check_quotas=True):
if self._committed:
# an order change can only be committed once
raise OrderError(error_messages['internal'])
@@ -1317,7 +1325,8 @@ class OrderChangeManager:
with self.order.event.lock():
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID):
raise OrderError(self.error_messages['not_pending_or_paid'])
self._check_quotas()
if check_quotas:
self._check_quotas()
self._check_complete_cancel()
self._perform_operations()
self._recalculate_total_and_payment_fee()
@@ -1409,7 +1418,7 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
else:
elif refund_amount != Decimal('0.00'):
notify_admin = True
if notify_admin:

View File

@@ -117,7 +117,7 @@ def get_tickets_for_order(order):
if p.multi_download_enabled:
try:
if len(order.positions_with_tickets) == 0:
if len(list(order.positions_with_tickets)) == 0:
continue
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False

View File

@@ -8,13 +8,13 @@
<h1>{% trans "Internal Server Error" %}</h1>
<p>{% trans "We had trouble processing your request." %}</p>
<p>{% trans "If this problem persists, please contact us." %}</p>
{% if request.sentry.id %}
{% if sentry_event_id %}
<p>
{% blocktrans trimmed %}
If you contact us, please send us the following code:
{% endblocktrans %}
<br>
{{ request.sentry.id }}
{{ sentry_event_id }}
</p>
{% endif %}
<p>{{ exception }}</p>

View File

@@ -84,7 +84,7 @@ def markdown_compile(source):
source,
extensions=[
'markdown.extensions.sane_lists',
# 'markdown.extensions.nl2br', # TODO: Enable, but check backwards-compatibility issues e.g. with mails
'markdown.extensions.nl2br'
]
),
tags=ALLOWED_TAGS,

View File

@@ -7,6 +7,7 @@ from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
def csrf_failure(request, reason=""):
@@ -65,5 +66,6 @@ def server_error(request):
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return HttpResponseServerError(template.render({
'request': request
'request': request,
'sentry_event_id': last_event_id(),
}))

View File

@@ -52,6 +52,15 @@ def contextprocessor(request):
ctx['has_domain'] = request.event.organizer.domains.exists()
if not request.event.testmode:
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
if complain_testmode_orders is None:
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
ctx['complain_testmode_orders'] = complain_testmode_orders
else:
ctx['complain_testmode_orders'] = False
if not request.event.live and ctx['has_domain']:
child_sess = request.session.get('child_session_{}'.format(request.event.pk))
s = SessionStore()

View File

@@ -973,6 +973,13 @@ class MailSettingsForm(SettingsForm):
self.fields['mail_html_renderer'].choices = [
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
keys = list(event.meta_data.keys())
for k, v in self.fields.items():
if k.startswith('mail_text_'):
v.help_text = str(v.help_text) + ', ' + ', '.join({
'{meta_' + p + '}' for p in keys
})
v.validators[0].limit_value += ['{meta_' + p + '}' for p in keys]
def clean(self):
data = self.cleaned_data

View File

@@ -207,10 +207,12 @@ class EventOrderFilterForm(OrderFilterForm):
(Order.STATUS_EXPIRED, _('Expired')),
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
(Order.STATUS_CANCELED, _('Canceled')),
('cp', _('Canceled (or with paid fee)')),
('pa', _('Approval pending')),
('overpaid', _('Overpaid')),
('underpaid', _('Underpaid')),
('pendingpaid', _('Pending (but fully paid)'))
('pendingpaid', _('Pending (but fully paid)')),
('testmode', _('Test mode')),
),
required=False,
)
@@ -244,10 +246,10 @@ class EventOrderFilterForm(OrderFilterForm):
qs = super().filter_qs(qs)
if fdata.get('item'):
qs = qs.filter(all_positions__item=fdata.get('item'), all_positions__canceled=False)
qs = qs.filter(all_positions__item=fdata.get('item'), all_positions__canceled=False).distinct()
if fdata.get('subevent'):
qs = qs.filter(all_positions__subevent=fdata.get('subevent'), all_positions__canceled=False)
qs = qs.filter(all_positions__subevent=fdata.get('subevent'), all_positions__canceled=False).distinct()
if fdata.get('question') and fdata.get('answer') is not None:
q = fdata.get('question')
@@ -297,6 +299,19 @@ class EventOrderFilterForm(OrderFilterForm):
status=Order.STATUS_PENDING,
require_approval=True
)
elif fdata.get('status') == 'testmode':
qs = qs.filter(
testmode=True
)
elif fdata.get('status') == 'cp':
s = OrderPosition.objects.filter(
order=OuterRef('pk')
)
qs = qs.annotate(
has_pc=Exists(s)
).filter(
Q(status=Order.STATUS_PAID, has_pc=False) | Q(status=Order.STATUS_CANCELED)
)
return qs

View File

@@ -160,6 +160,7 @@ class ItemCreateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
@@ -239,6 +240,9 @@ class ItemCreateForm(I18nModelForm):
if self.cleaned_data.get('quota_option') == self.EXISTING and self.cleaned_data.get('quota_add_existing') is not None:
quota = self.cleaned_data.get('quota_add_existing')
quota.items.add(self.instance)
quota.log_action('pretix.event.quota.changed', user=self.user, data={
'item_added': self.instance.pk
})
elif self.cleaned_data.get('quota_option') == self.NEW:
quota_name = self.cleaned_data.get('quota_add_new_name')
quota_size = self.cleaned_data.get('quota_add_new_size')
@@ -247,6 +251,11 @@ class ItemCreateForm(I18nModelForm):
event=self.event, name=quota_name, size=quota_size
)
quota.items.add(self.instance)
quota.log_action('pretix.event.quota.added', user=self.user, data={
'name': quota_name,
'size': quota_size,
'items': [self.instance.pk]
})
if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:

View File

@@ -177,6 +177,10 @@ class OtherOperationsForm(forms.Form):
'Send an email to the customer notifying that their order has been changed.'
)
)
ignore_quotas = forms.BooleanField(
label=_('Allow to overbook quotas when performing this operation'),
required=False,
)
def __init__(self, *args, **kwargs):
kwargs.pop('order')
@@ -286,6 +290,7 @@ class OrderPositionChangeForm(forms.Form):
('secret', 'Regenerate secret'),
)
)
change_product_keep_price = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')

View File

@@ -172,6 +172,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.canceled': _('The order has been canceled.'),
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.order.placed': _('The order has been created.'),
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
@@ -268,6 +269,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.plugins.disabled': _('A plugin has been disabled.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event settings have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),

View File

@@ -235,6 +235,14 @@
</ul>
</div>
<ul class="nav" id="side-menu">
{% if request.event and request.event.testmode %}
<li class="testmode">
<a href="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}">
<i class="fa fa-warning fa-fw"></i>
{% trans "TEST MODE" %}
</a>
</li>
{% endif %}
{% block nav %}
{% for nav in nav_items %}
<li>
@@ -317,6 +325,20 @@
</div>
{% endfor %}
{% endif %}
{% if complain_testmode_orders %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
Your event contains <strong>test mode orders</strong> even though <strong>test mode has been disabled</strong>.
You should delete those orders to make sure they do not show up in your reports and statistics and block people from
actually buying tickets.
{% endblocktrans %}
<strong>
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.organizer.slug %}?status=testmode">
{% trans "Show all test mode orders" %}
</a>
</strong>
</div>
{% endif %}
{% if warning_update_check_active %}
<div class="alert alert-info">
<a href="{% url "control:global.update" %}">

View File

@@ -21,7 +21,7 @@
<span class="fa fa-download"></span>
{% trans "PDF" %}
</a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistcsv&checkinlistcsv-list={{ checkinlist.pk }}"
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank">
<span class="fa fa-download"></span>
{% trans "CSV" %}
@@ -89,6 +89,9 @@
{% if e.order.status == "n" %}
<span class="label label-warning">{% trans "unpaid" %}</span>
{% endif %}
{% if e.order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>{{ e.item }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
<td>{{ e.order.email }}</td>

View File

@@ -3,49 +3,113 @@
{% load bootstrap3 %}
{% block content %}
<h1>{% trans "Shop status" %}</h1>
{% if request.event.live %}
<p>
{% trans "Your shop is currently live. If you take it down, it will only be visible to you and your team." %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="live" value="false">
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Go offline" %}
</button>
</div>
</form>
{% else %}
<p>
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
</p>
{% if issues|length > 0 %}
<div class="alert alert-warning">
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Shop visibility" %}
</div>
<div class="panel-body">
{% if request.event.live %}
<p>
{% trans "To publish your ticket shop, you first need to resolve the following issues:" %}
{% trans "Your shop is currently live. If you take it down, it will only be visible to you and your team." %}
</p>
<ul>
{% for issue in issues %}
<li>{{ issue|safe }}</li>
{% endfor %}
</ul>
</div>
{% else %}
<p>
{% trans "If you want to, you can publish your ticket shop now." %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="live" value="true">
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Go live" %}
<form action="" method="post" class="text-right">
{% csrf_token %}
<input type="hidden" name="live" value="false">
<button type="submit" class="btn btn-lg btn-danger btn-save">
{% trans "Go offline" %}
</button>
</div>
</form>
{% endif %}
{% endif %}
</form>
{% else %}
{% if issues|length > 0 %}
<p>
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
</p>
<div class="alert alert-warning">
<p>
{% trans "To publish your ticket shop, you first need to resolve the following issues:" %}
</p>
<ul>
{% for issue in issues %}
<li>{{ issue|safe }}</li>
{% endfor %}
</ul>
</div>
<div class="test-right">
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
{% trans "Go live" %}
</button>
</div>
{% else %}
<p>
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
</p>
<p>
{% trans "If you want to, you can publish your ticket shop now." %}
</p>
<form action="" method="post" class="text-right">
{% csrf_token %}
<input type="hidden" name="live" value="true">
<button type="submit" class="btn btn-primary btn-lg btn-save">
{% trans "Go live" %}
</button>
</form>
{% endif %}
{% endif %}
<div class="clear"></div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Test mode" %}
</div>
<div class="panel-body">
{% if request.event.testmode %}
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="testmode" value="false">
<p>
{% trans "Your shop is currently in test mode. All orders are not persistant and can be deleted at any point." %}
</p>
<div class="form-inline">
<label class="checkbox">
<input type="checkbox" name="delete" value="yes" />
{% trans "Permanently delete all orders created in test mode" %}
</label>
</div>
<div class="text-right">
<button type="submit" class="btn btn-lg btn-primary btn-save">
{% trans "Disable test mode" %}
</button>
</div>
</form>
{% else %}
<p>
{% trans "Your shop is currently in production mode." %}
</p>
<p>
{% trans "If you want to do some test orders, you can enable test mode for your shop. As long as the shop is in test mode, all orders that are created are marked as test orders and can be deleted again." %}
<strong>
{% trans "Please note that test orders still count into your quotas, actually use vouchers and might perform actual payments. The only difference is that you can delete test orders. Use at your own risk!" %}
</strong>
</p>
<p>
{% trans "Also, test mode only covers the main web shop. Orders created through other sales channels such as the box office or resellers module are still created as production orders." %}
</p>
{% if actual_orders %}
<div class="alert alert-danger">
{% trans "It looks like you already have some real orders in your shop. We do not recommend enabling test mode if your customers already know your shop, as it will confuse them." %}
</div>
{% endif %}
<form action="" method="post" class="text-right">
{% csrf_token %}
<input type="hidden" name="testmode" value="true">
<button type="submit" class="btn btn-danger btn-lg btn-save">
{% trans "Enable test mode" %}
</button>
</form>
{% endif %}
<div class="clear"></div>
</div>
</div>
{% endblock %}

View File

@@ -90,6 +90,11 @@
<span class="fa fa-eraser"></span>
{% trans "Delete personal data" %}
</a>
<a href="{% url "control:events.add" %}?clone={{ request.event.pk }}"
class="btn btn-default btn-lg">
<span class="fa fa-copy"></span>
{% trans "Clone event" %}
</a>
</div>
</div>
</form>

View File

@@ -10,6 +10,7 @@
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{{ wizard.management_form }}
{{ wizard.prefix_form }}
{% bootstrap_form_errors form %}
{% block form %}
{% endblock %}

View File

@@ -72,11 +72,13 @@
<a href="?{% url_replace request 'ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th class="text-right">
<th>
{% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-live' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'live' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th class="text-right">
</th>
</tr>
</thead>
<tbody>
@@ -117,7 +119,7 @@
</a>
{% endif %}
</td>
<td class="text-right">
<td>
{% if not e.live %}
<span class="label label-danger">{% trans "Shop disabled" %}</span>
{% elif e.presale_has_ended %}
@@ -128,6 +130,17 @@
<span class="label label-success">{% trans "On sale" %}</span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}"
class="btn btn-sm btn-default" title="{% trans "Open event dashboard" %}"
data-toggle="tooltip">
<span class="fa fa-eye"></span>
</a>
<a href="{% url "control:events.add" %}?clone={{ e.pk }}" class="btn btn-sm btn-default"
title="{% trans "Clone event" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -95,6 +95,10 @@
{% trans "Change product to" %}
{% bootstrap_field position.form.itemvar layout='inline' %}
</label>
<label class="checkbox">
{{ position.form.change_product_keep_price }}
{% trans "Keep price the same" %}
</label>
</div>
{% if request.event.has_subevents %}
<div class="radio">
@@ -202,6 +206,9 @@
</div>
</div>
</div>
<div class="">
{% bootstrap_field other_form.ignore_quotas layout="" %}
</div>
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">

View File

@@ -0,0 +1,31 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Delete order" %}
{% endblock %}
{% block content %}
<h1>
{% trans "Delete order" %}
</h1>
<p>{% blocktrans trimmed %}
Do you really want to delete this order? <strong>You really cannot revert this action and we can't either.</strong>
{% endblocktrans %}</p>
<form method="post" href="">
{% csrf_token %}
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "No, take me back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-danger btn-lg" type="submit">
{% trans "Yes, delete order" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -16,6 +16,9 @@
{% blocktrans trimmed with code=order.code %}
Order details: {{ code }}
{% endblocktrans %}
{% if order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right" %}
</h1>
{% if 'can_change_orders' in request.eventpermset %}
@@ -24,6 +27,13 @@
{% csrf_token %}
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
{% if order.testmode %}
<a href="{% url "control:event.order.delete" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
class="btn btn-danger">
<span class="fa fa-trash"></span>
{% trans "Delete" %}
</a>
{% endif %}
{% if order.require_approval and order.status == 'n' %}
<a href="{% url "control:event.order.approve" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
class="btn btn-primary">
@@ -270,6 +280,10 @@
</form>
{% endfor %}
{% endif %}
<button type="submit" data-toggle="qrcode" data-qrcode="{{ line.secret }}"
class="btn btn-xs btn-default">
<span class="fa fa-qrcode"></span> {% trans "Show ticket code" %}
</button>
{% eventsignal event "pretix.control.signals.order_position_buttons" order=order position=line request=request %}
</div>
{% endif %}

View File

@@ -114,9 +114,11 @@
<strong>
<a
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
{{ o.code }}
</a>
{{ o.code }}</a>
</strong>
{% if o.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>
{{ o.email|default_if_none:"" }}

View File

@@ -32,7 +32,7 @@
<th>{% trans "Product" %}</th>
<th>{% trans "Canceled" %}¹</th>
<th>{% trans "Expired" %}</th>
<th colspan="3">{% trans "Purchased" %}</th>
<th colspan="3" class="text-center">{% trans "Purchased" %}</th>
</tr>
<tr>
<th></th>

View File

@@ -55,6 +55,9 @@
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code %}">
{{ r.order.code }}</a>-R-{{ r.local_id }}
</strong>
{% if r.order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>
{{ r.payment_provider.verbose_name }}

View File

@@ -15,6 +15,7 @@
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
<th></th>
</tr>
</thead>
<tbody>
@@ -25,13 +26,28 @@
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
<td class="text-right">
<a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}"
class="btn btn-sm btn-default" title="{% trans "Open event dashboard" %}"
data-toggle="tooltip">
<span class="fa fa-eye"></span>
</a>
{% if "can_create_events" in request.orgapermset %}
<a href="{% url "control:events.add" %}?clone={{ e.pk }}" class="btn btn-sm btn-default"
title="{% trans "Clone event" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
{% if "can_create_events" in request.orgapermset %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
{% endif %}
{% endblock %}

View File

@@ -59,9 +59,11 @@
<td>
<strong>
<a href="{% url "control:event.order" event=o.event.slug organizer=o.event.organizer.slug code=o.code %}">
{{ o.event.slug|upper }}-{{ o.code }}
</a>
{{ o.event.slug|upper }}-{{ o.code }}</a>
</strong>
{% if o.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>{{ o.event.name }}</td>
<td>

View File

@@ -222,6 +222,8 @@ urlpatterns = [
name='event.order.approve'),
url(r'^orders/(?P<code>[0-9A-Z]+)/deny$', orders.OrderDeny.as_view(),
name='event.order.deny'),
url(r'^orders/(?P<code>[0-9A-Z]+)/delete$', orders.OrderDelete.as_view(),
name='event.order.delete'),
url(r'^orders/(?P<code>[0-9A-Z]+)/info', orders.OrderModifyInformation.as_view(),
name='event.order.info'),
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),

View File

@@ -135,14 +135,33 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/checkin/lists.html'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related("limit_products")
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
if self.request.GET.get("subevent", "") != "":
s = self.request.GET.get("subevent", "")
qs = qs.filter(subevent_id=s)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
clists = list(ctx['checkinlists'])
# Optimization: Fetch expensive columns for this page only
annotations = {
a['pk']: a
for a in CheckinList.annotate_with_numbers(CheckinList.objects.filter(pk__in=[l.pk for l in clists]), self.request.event).values(
'pk', 'checkin_count', 'position_count', 'percent'
)
}
for cl in clists:
if cl.subevent:
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
cl.checkin_count = annotations.get(cl.pk, {}).get('checkin_count', 0)
cl.position_count = annotations.get(cl.pk, {}).get('position_count', 0)
cl.percent_count = annotations.get(cl.pk, {}).get('percent_count', 0)
ctx['checkinlists'] = clists
return ctx
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
model = CheckinList

View File

@@ -183,8 +183,20 @@ def shop_state_widget(sender, **kwargs):
'priority': 1000,
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
t1=_('Your ticket shop is'), t2=_('Click here to change'),
state=_('live') if sender.live else _('not yet public'),
icon='fa-check-circle' if sender.live else 'fa-times-circle',
state=_('live') if sender.live and not sender.testmode else (
_('live and in test mode') if sender.live else (
_('not yet public') if not sender.testmode else (
_('in private test mode')
)
)
),
icon='fa-check-circle' if sender.live and not sender.testmode else (
'fa-warning' if sender.live else (
'fa-times-circle' if not sender.testmode else (
'fa-times-circle'
)
)
),
cls='live' if sender.live else 'off'
),
'url': reverse('control:event.live', kwargs={

View File

@@ -573,9 +573,13 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
locales[str(idx)] = val[0]
return locales
@cached_property
def meta_properties(self):
return [p.name for p in self.request.organizer.meta_properties.all()]
@cached_property
def items(self):
return {
kv = {
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company', 'total_with_currency',
'event', 'payment_info', 'url', 'invoice_name'],
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
@@ -596,6 +600,10 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'mail_text_order_denied': ['total', 'currency', 'date', 'invoice_company',
'total_with_currency', 'event', 'url', 'invoice_name'],
}
for v in kv.values():
for p in self.meta_properties:
v.append('meta_' + p)
return kv
@cached_property
def base_data(self):
@@ -607,7 +615,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
orders = [' - {} - {}'.format(self.generate_order_fullname(self.request.event.slug, order['code']),
self.generate_order_url(order['code'], order['secret']))
for order in user_orders]
return {
d = {
'event': self.request.event.name,
'total': 42.23,
'total_with_currency': LazyCurrencyNumber(42.23, self.request.event.currency),
@@ -620,8 +628,11 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
'invoice_name': _('John Doe'),
'invoice_company': _('Sample Corporation'),
'common': _('An individial text with a reason can be inserted here.'),
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999')
'payment_info': _('Please transfer money to this bank account: 9999-9999-9999-9999'),
}
for k, v in self.request.event.meta_data.items():
d['meta_' + k] = v
return d
def generate_order_url(self, code, secret):
return build_absolute_uri('presale:event.order', kwargs={
@@ -679,7 +690,8 @@ class MailSettingsRendererPreview(MailSettingsPreview):
expires=now(), code="PREVIEW", total=119)
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
description=ugettext("Sample product description"))
order.positions.create(item=item, attendee_name_parts={'full_name': ugettext("John Doe")}, price=item.default_price)
order.positions.create(item=item, attendee_name_parts={'_legacy': ugettext("John Doe")},
price=item.default_price)
v = renderers[request.GET.get('renderer')].render(
v,
str(request.event.settings.mail_text_signature),
@@ -842,23 +854,55 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['issues'] = self.request.event.live_issues
ctx['actual_orders'] = self.request.event.orders.filter(testmode=False).exists()
return ctx
def post(self, request, *args, **kwargs):
if request.POST.get("live") == "true" and not self.request.event.live_issues:
request.event.live = True
request.event.save()
self.request.event.log_action(
'pretix.event.live.activated', user=self.request.user, data={}
)
with transaction.atomic():
request.event.live = True
request.event.save()
self.request.event.log_action(
'pretix.event.live.activated', user=self.request.user, data={}
)
messages.success(self.request, _('Your shop is live now!'))
elif request.POST.get("live") == "false":
request.event.live = False
request.event.save()
self.request.event.log_action(
'pretix.event.live.deactivated', user=self.request.user, data={}
)
with transaction.atomic():
request.event.live = False
request.event.save()
self.request.event.log_action(
'pretix.event.live.deactivated', user=self.request.user, data={}
)
messages.success(self.request, _('We\'ve taken your shop down. You can re-enable it whenever you want!'))
elif request.POST.get("testmode") == "true":
with transaction.atomic():
request.event.testmode = True
request.event.save()
self.request.event.log_action(
'pretix.event.testmode.activated', user=self.request.user, data={}
)
messages.success(self.request, _('Your shop is now in test mode!'))
elif request.POST.get("testmode") == "false":
with transaction.atomic():
request.event.testmode = False
request.event.save()
self.request.event.log_action(
'pretix.event.testmode.deactivated', user=self.request.user, data={
'delete': (request.POST.get("delete") == "yes")
}
)
request.event.cache.delete('complain_testmode_orders')
if request.POST.get("delete") == "yes":
try:
with transaction.atomic():
for order in request.event.orders.filter(testmode=True):
order.gracefully_delete(user=self.request.user)
except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.'))
else:
request.event.cache.set('complain_testmode_orders', False, 30)
messages.success(self.request, _('We\'ve disabled test mode for you. Let\'s sell some real tickets!'))
return redirect(self.get_success_url())
def get_success_url(self) -> str:

View File

@@ -817,7 +817,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
"""
newinst = Item(event=self.request.event)
kwargs = super().get_form_kwargs()
kwargs.update({'instance': newinst})
kwargs.update({'instance': newinst, 'user': self.request.user})
return kwargs
def form_invalid(self, form):

View File

@@ -1,4 +1,5 @@
from django.conf import settings
from django.contrib import messages
from django.db import transaction
from django.db.models import (
F, IntegerField, Max, Min, OuterRef, Prefetch, Subquery, Sum,
@@ -12,9 +13,9 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext, ugettext_lazy as _
from django.views import View
from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from i18nfield.strings import LazyI18nString
from pretix.base.forms import SafeSessionWizardView
from pretix.base.i18n import language
from pretix.base.models import Event, Organizer, Quota, Team
from pretix.control.forms.event import (
@@ -94,10 +95,13 @@ class EventList(PaginationMixin, ListView):
def condition_copy(wizard):
return EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
return (
not wizard.clone_from and
EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
)
class EventWizard(SessionWizardView):
class EventWizard(SafeSessionWizardView):
form_list = [
('foundation', EventWizardFoundationForm),
('basics', EventWizardBasicsForm),
@@ -112,6 +116,49 @@ class EventWizard(SessionWizardView):
'copy': condition_copy
}
def get_form_initial(self, step):
initial = super().get_form_initial(step)
if self.clone_from:
if step == 'foundation':
initial['organizer'] = self.clone_from.organizer
initial['locales'] = self.clone_from.settings.locales
initial['has_subevents'] = self.clone_from.has_subevents
elif step == 'basics':
initial['name'] = self.clone_from.name
initial['slug'] = self.clone_from.slug + '-2'
initial['currency'] = self.clone_from.currency
initial['date_from'] = self.clone_from.date_from
initial['date_to'] = self.clone_from.date_to
initial['presale_start'] = self.clone_from.presale_start
initial['presale_end'] = self.clone_from.presale_end
initial['location'] = self.clone_from.location
initial['timezone'] = self.clone_from.settings.timezone
initial['locale'] = self.clone_from.settings.locale
if self.clone_from.settings.tax_rate_default:
initial['tax_rate'] = self.clone_from.settings.tax_rate_default.rate
return initial
def dispatch(self, request, *args, **kwargs):
self.clone_from = None
if 'clone' in self.request.GET:
try:
clone_from = Event.objects.select_related('organizer').get(pk=self.request.GET.get("clone"))
except Event.DoesNotExist:
allow = False
else:
allow = (
request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_event_settings', request)
and request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_items', request)
)
if not allow:
messages.error(self.request, _('You do not have permission to clone this event.'))
else:
self.clone_from = clone_from
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, form, **kwargs):
ctx = super().get_context_data(form, **kwargs)
ctx['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists()
@@ -133,6 +180,13 @@ class EventWizard(SessionWizardView):
}
if step != 'foundation':
fdata = self.get_cleaned_data_for_step('foundation')
if fdata is None:
fdata = {
'organizer': Organizer(slug='_nonexisting'),
'has_subevents': False,
'locales': ['en']
}
# The show must go on, we catch this error in render()
kwargs.update(fdata)
return kwargs
@@ -186,6 +240,8 @@ class EventWizard(SessionWizardView):
if copy_data and copy_data['copy_from_event']:
from_event = copy_data['copy_from_event']
event.copy_data_from(from_event)
elif self.clone_from:
event.copy_data_from(self.clone_from)
elif event.has_subevents:
event.checkin_lists.create(
name=str(se),
@@ -209,7 +265,7 @@ class EventWizard(SessionWizardView):
event.settings.set('locale', basics_data['locale'])
event.settings.set('locales', foundation_data['locales'])
if (copy_data and copy_data['copy_from_event']) or event.has_subevents:
if (copy_data and copy_data['copy_from_event']) or self.clone_from or event.has_subevents:
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,

View File

@@ -11,7 +11,9 @@ from django.conf import settings
from django.contrib import messages
from django.core.files import File
from django.db import transaction
from django.db.models import Count, IntegerField, OuterRef, Subquery
from django.db.models import (
Count, IntegerField, OuterRef, ProtectedError, Subquery,
)
from django.http import (
FileResponse, Http404, HttpResponseNotAllowed, JsonResponse,
)
@@ -398,6 +400,35 @@ class OrderApprove(OrderView):
})
class OrderDelete(OrderView):
permission = 'can_change_orders'
def post(self, *args, **kwargs):
if self.order.testmode:
try:
with transaction.atomic():
self.order.gracefully_delete(user=self.request.user)
messages.success(self.request, _('The order has been deleted.'))
return redirect(reverse('control:event.orders', kwargs={
'event': self.request.event.slug,
'organizer': self.request.organizer.slug,
}))
except ProtectedError:
messages.error(self.request, _('The order could not be deleted as some constraints (e.g. data created '
'by plug-ins) do not allow it.'))
return self.get(self.request, *self.args, **self.kwargs)
return redirect(self.get_order_url())
def get(self, *args, **kwargs):
if not self.order.testmode:
messages.error(self.request, _('Only orders created in test mode can be deleted.'))
return redirect(self.get_order_url())
return render(self.request, 'pretixcontrol/order/delete.html', {
'order': self.order,
})
class OrderDeny(OrderView):
permission = 'can_change_orders'
@@ -1229,7 +1260,7 @@ class OrderChange(OrderView):
variation = ItemVariation.objects.get(pk=varid, item=item)
else:
variation = None
ocm.change_item(p, item, variation)
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price'])
elif p.form.cleaned_data['operation'] == 'price':
ocm.change_price(p, p.form.cleaned_data['price'])
elif p.form.cleaned_data['operation'] == 'subevent':
@@ -1259,7 +1290,7 @@ class OrderChange(OrderView):
messages.error(self.request, _('An error occurred. Please see the details below.'))
else:
try:
ocm.commit()
ocm.commit(check_quotas=not self.other_form.cleaned_data['ignore_quotas'])
except OrderError as e:
messages.error(self.request, str(e))
else:

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.db.models import Count, IntegerField, OuterRef, Q, Subquery
from django.utils.functional import cached_property
from django.views.generic import ListView
@@ -28,7 +29,7 @@ class OrderSearch(PaginationMixin, ListView):
annotated = {
o['pk']: o
for o in
Order.objects.filter(
Order.objects.using(settings.DATABASE_REPLICA).filter(
pk__in=[o.pk for o in ctx['orders']]
).annotate(
pcnt=Subquery(s, output_field=IntegerField())
@@ -45,7 +46,7 @@ class OrderSearch(PaginationMixin, ListView):
return ctx
def get_queryset(self):
qs = Order.objects.select_related('invoice_address')
qs = Order.objects.using(settings.DATABASE_REPLICA)
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
@@ -58,9 +59,47 @@ class OrderSearch(PaginationMixin, ListView):
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.filter_form.cleaned_data.get('query'):
"""
We need to work around a bug in PostgreSQL's (and likely MySQL's) query plan optimizer here.
The database lacks statistical data to predict how common our search filter is and therefore
assumes that it is cheaper to first ORDER *all* orders in the system (since we got an index on
datetime), then filter out with a full scan until OFFSET/LIMIT condition is fulfilled. If we
look for something rare (such as an email address used once within hundreds of thousands of
orders, this ends up to be pathologically slow.
For some search queries on pretix.eu, we see search times of >30s, just due to the ORDER BY and
LIMIT clause. Without them. the query runs in roughly 0.6s. This heuristical approach tries to
detect these cases and rewrite the query as a nested subquery that strongly suggests sorting
before filtering. However, since even that fails in some cases because PostgreSQL thinks it knows
better, we literally force it by evaluating the subquery explicitly. We only do this for n<=200,
to avoid memory leaks and problems with maximum parameter count on SQLite. In cases where the
search query yields lots of results, this will actually be slower since it requires two queries,
sorry.
Phew.
"""
page = self.kwargs.get(self.page_kwarg) or self.request.GET.get(self.page_kwarg) or 1
limit = self.get_paginate_by(None)
offset = (page - 1) * limit
resultids = list(qs.order_by().values_list('id', flat=True)[:201])
if len(resultids) <= 200 and len(resultids) <= offset + limit:
qs = Order.objects.using(settings.DATABASE_REPLICA).filter(
id__in=resultids
)
"""
We use prefetch_related here instead of select_related for a reason, even though select_related
would be the common choice for a foreign key and doesn't require an additional database query.
The problem is, that if our results contain the same event 25 times, select_related will create
25 Django objects which will all try to pull their ownsettings cache to show the event properly,
leading to lots of unnecessary queries. Due to the way prefetch_related works differently, it
will only create one shared Django object.
"""
return qs.only(
'id', 'invoice_address__name_cached', 'invoice_address__name_parts', 'code', 'event', 'email',
'datetime', 'total', 'status'
'datetime', 'total', 'status', 'require_approval', 'testmode'
).prefetch_related(
'event', 'event__organizer'
)
).select_related('invoice_address')

View File

@@ -105,7 +105,10 @@ class VoucherTags(EventPermissionRequiredMixin, TemplateView):
redeemed=Sum('redeemed')
)
for t in tags:
t['percentage'] = int((t['redeemed'] / t['total']) * 100)
if t['total'] == 0:
t['percentage'] = 0
else:
t['percentage'] = int((t['redeemed'] / t['total']) * 100)
ctx['tags'] = tags
return ctx
@@ -140,6 +143,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
messages.error(request, _('A voucher can not be deleted if it already has been redeemed.'))
else:
self.object.log_action('pretix.voucher.deleted', user=self.request.user)
self.object.cartposition_set.all().delete()
self.object.delete()
messages.success(request, _('The selected voucher has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -344,6 +348,7 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
for obj in self.objects:
if obj.allow_delete():
obj.log_action('pretix.voucher.deleted', user=self.request.user)
obj.cartposition_set.all().delete()
obj.delete()
else:
obj.log_action('pretix.voucher.changed', user=self.request.user, data={

View File

@@ -32,7 +32,7 @@ def get_sizes(size, imgsize):
(0, int((imgsize[1] * wfactor - imgsize[1] * hfactor) / 2),
imgsize[0] * hfactor, int((imgsize[1] * wfactor + imgsize[1] * wfactor) / 2))
elif wfactor > hfactor:
return (int(size[0]), int(imgsize[1] * hfactor)), \
return (int(size[0]), int(imgsize[1] * wfactor)), \
(0, int((imgsize[1] * wfactor - size[1]) / 2), size[0], int((imgsize[1] * wfactor + size[1]) / 2))
else:
return (int(imgsize[0] * hfactor), int(size[1])), \

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2018-12-02 15:44+0000\n"
"Last-Translator: Alexander Schwartz <alexander.schwartz@gmx.net>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2018-12-02 13:41+0000\n"
"Last-Translator: Alexander Schwartz <alexander.schwartz@gmx.net>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+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"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-02-01 16:27+0000\n"
"PO-Revision-Date: 2018-11-29 09:01+0000\n"
"Last-Translator: arabestia <sergioadalbertor@gmail.com>\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2019-02-20 03:00+0000\n"
"Last-Translator: oocf <oswaldocerna@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
"Language: es\n"
@@ -17,7 +17,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 3.1.1\n"
"X-Generator: Weblate 3.4\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -62,19 +62,14 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:45
#: pretix/static/pretixbase/js/asynctask.js:101
#, 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 "
"browser and try again."
msgstr ""
"Su solicitud ha sido enviada al servidor y se procesada en breve. Si el "
"proceso tomara más de dos minutos, por favor, contáctenos o regrese a la "
"página anterior en su navegador y pruebe de nuevo."
"Su solicitud llegó al servidor pero seguimos esperando a que sea procesada. "
"Si toma más de dos minutos, por favor contáctenos o regrese a la página "
"anterior en su navegador y pruebe de nuevo."
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2018-10-28 10:23+0000\n"
"Last-Translator: Arnaud Vergnet <keplyx@gmail.com>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+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-"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2019-01-08 12:30+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2019-01-08 21:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2018-10-22 04:23+0000\n"
"Last-Translator: Samir C. Costa <samirfor@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"

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-02-01 16:27+0000\n"
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
"PO-Revision-Date: 2019-01-02 08:21+0000\n"
"Last-Translator: Alexey Zh <write2aracon@gmail.com>\n"
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix-"

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