forked from CGM_Public/pretix_original
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc6d03a8b | ||
|
|
6785979fbc | ||
|
|
23958b3d03 | ||
|
|
831e31ea9d | ||
|
|
66483b6ae8 | ||
|
|
4614d04be4 | ||
|
|
1285e9aa69 | ||
|
|
d108cec685 | ||
|
|
764b9dda7e | ||
|
|
82d289cfcf | ||
|
|
184c91cfbc | ||
|
|
10103b58f0 | ||
|
|
2678100149 | ||
|
|
09a9dfe591 | ||
|
|
af3e8d5515 | ||
|
|
1b72eca5ec | ||
|
|
df5968660b | ||
|
|
eb04e1dcee | ||
|
|
7dff5001b0 | ||
|
|
ca93673c10 | ||
|
|
71a4664d1f | ||
|
|
429f30fca7 | ||
|
|
5376ce4bdb | ||
|
|
96b57994d9 | ||
|
|
d1971cdcae | ||
|
|
65116563fd | ||
|
|
d811e42095 | ||
|
|
2a7e185d2e | ||
|
|
1a894d71b8 | ||
|
|
9213a40219 | ||
|
|
bf8a6ebbf8 | ||
|
|
2bcb0b0ac1 | ||
|
|
9767243a6d | ||
|
|
df7fbe5a66 | ||
|
|
c16dd0c9b6 | ||
|
|
f5c47424f3 | ||
|
|
6207662ca5 | ||
|
|
d63cc80507 | ||
|
|
b857157c7b | ||
|
|
2b8d12f987 | ||
|
|
28682c7c33 | ||
|
|
fe61e4f3e2 | ||
|
|
7916e81745 | ||
|
|
4e6fb7799a | ||
|
|
03dd0e530e | ||
|
|
cb6f6247fd | ||
|
|
c33fc7630e | ||
|
|
2910160af9 | ||
|
|
3b2247de39 | ||
|
|
60212dcbcc | ||
|
|
1b8b12cbc3 | ||
|
|
e57ab7f030 | ||
|
|
2f13fa79ba | ||
|
|
c616c8ce29 | ||
|
|
0f2b56adb4 | ||
|
|
a2ba0f8b9f | ||
|
|
c6a7b52e34 | ||
|
|
64b67e5396 | ||
|
|
ab2084692d | ||
|
|
03133dc1fd | ||
|
|
7e1e259897 | ||
|
|
6720c0e993 | ||
|
|
53bb2b2945 | ||
|
|
a2c5ce5ebc | ||
|
|
b4928f662a | ||
|
|
b9b509ad9b | ||
|
|
d93ad8044a | ||
|
|
9d14e8113f | ||
|
|
84d1d758c1 | ||
|
|
cbfd722c92 | ||
|
|
be6496e569 | ||
|
|
de086a2b07 | ||
|
|
3f8df0f036 | ||
|
|
b2c49aa786 | ||
|
|
a0e7bd3996 | ||
|
|
e06be9ee25 | ||
|
|
07473f854e | ||
|
|
f342e46f53 | ||
|
|
d3a287dcdf | ||
|
|
ce2101a8e1 | ||
|
|
2d456a6dc4 | ||
|
|
a3e0e14cef | ||
|
|
bbade75061 | ||
|
|
645e82fb04 | ||
|
|
3245b05c5f | ||
|
|
61ef81832d | ||
|
|
7dea6fc1b7 | ||
|
|
bd306e9400 | ||
|
|
3e686211e1 | ||
|
|
6d1b4b0a39 | ||
|
|
58938fc07c | ||
|
|
96dd4e02f3 | ||
|
|
411c537438 | ||
|
|
bbd112280a | ||
|
|
28d074366e | ||
|
|
11d76656de | ||
|
|
1c96bc31d5 | ||
|
|
0030064f55 | ||
|
|
4726f5c136 | ||
|
|
c7fafedc51 | ||
|
|
3eeb70ae36 | ||
|
|
29b1a3dca3 | ||
|
|
caf844b5fb | ||
|
|
6b7bdf8c4f | ||
|
|
aad433a3bc | ||
|
|
3f1bb56826 | ||
|
|
b2b3add616 | ||
|
|
2d484d4a8e | ||
|
|
2f252f19c9 | ||
|
|
a27f372785 | ||
|
|
f074e642ec | ||
|
|
217ed905d4 | ||
|
|
b920efc955 | ||
|
|
330fadbea9 | ||
|
|
50c595e3d6 | ||
|
|
26f258c6cf | ||
|
|
f15a72e59d | ||
|
|
8accaae6b1 | ||
|
|
d4259501af | ||
|
|
fd5d5ae98e | ||
|
|
457901ff82 | ||
|
|
e201be1c65 | ||
|
|
acde14372d | ||
|
|
79988a2325 | ||
|
|
784f6e703c | ||
|
|
29b157f287 | ||
|
|
c030bd35ca | ||
|
|
06fe076ce2 | ||
|
|
ae6cba067c | ||
|
|
72ae19a95d | ||
|
|
1f889be07a | ||
|
|
39061b659a | ||
|
|
d38f29ac7c | ||
|
|
1a8e67f4de | ||
|
|
8265c302ad | ||
|
|
110d7c6acf | ||
|
|
244b767f8f | ||
|
|
f40950efc9 | ||
|
|
0e0534c273 | ||
|
|
9b3ea3656f | ||
|
|
62b2a367ff | ||
|
|
ab9dd32902 | ||
|
|
43fc498297 | ||
|
|
ef3eee7873 | ||
|
|
9f0deea9dd | ||
|
|
e3798600ed | ||
|
|
00834cd5e0 | ||
|
|
ed35c4f74e | ||
|
|
9cd3e2d494 | ||
|
|
3345f48986 | ||
|
|
b611d63975 | ||
|
|
fb3866aa1a | ||
|
|
a9f131b645 | ||
|
|
e5728662c5 | ||
|
|
94a97fb0fd | ||
|
|
b5bea6fe7a | ||
|
|
fb9d677d76 | ||
|
|
7c4fc7bd0d | ||
|
|
de992cecf3 | ||
|
|
cd94549606 | ||
|
|
214a6eb5ce | ||
|
|
db5f0aa02d | ||
|
|
ba48ab3659 | ||
|
|
d1538e07d3 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -5,6 +5,7 @@ src/static/moment/* linguist-vendored
|
||||
src/static/datetimepicker/* linguist-vendored
|
||||
src/static/colorpicker/* linguist-vendored
|
||||
src/static/fileupload/* linguist-vendored
|
||||
src/static/vuejs/* linguist-vendored
|
||||
src/static/charts/* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||
|
||||
40
.travis.yml
40
.travis.yml
@@ -12,29 +12,29 @@ services:
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=style
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
- python: 3.6
|
||||
env: JOB=tests-cov
|
||||
- python: 3.6
|
||||
env: JOB=style
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
||||
@@ -44,7 +44,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -90,7 +90,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -54,7 +54,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -103,7 +103,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "Sample Conference"},
|
||||
|
||||
@@ -41,6 +41,7 @@ foreign_currency_rate decimal (string) If ``foreign_cu
|
||||
invoicing time, it is stored here.
|
||||
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
||||
date at which the currency rate was obtained.
|
||||
internal_reference string Customer's reference to be printed on the invoice.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -57,6 +58,11 @@ foreign_currency_rate_date date If ``foreign_cu
|
||||
``foreign_currency_rate_date`` have been added.
|
||||
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The attribute ``internal_reference`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -78,7 +84,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -95,6 +101,7 @@ Endpoints
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
"introductory_text": "thank you for your purchase of the following items:",
|
||||
"internal_reference": "",
|
||||
"additional_text": "We are looking forward to see you on our conference!",
|
||||
"payment_provider_text": "Please transfer the money to our account ABC…",
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
@@ -146,7 +153,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"number": "SAMPLECONF-00001",
|
||||
@@ -158,6 +165,7 @@ Endpoints
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
"introductory_text": "thank you for your purchase of the following items:",
|
||||
"internal_reference": "",
|
||||
"additional_text": "We are looking forward to see you on our conference!",
|
||||
"payment_provider_text": "Please transfer the money to our account ABC…",
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
|
||||
@@ -101,7 +101,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -188,7 +188,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -43,6 +43,7 @@ invoice_address object Invoice address
|
||||
├ zipcode string Customer ZIP code
|
||||
├ city string Customer city
|
||||
├ country string Customer country
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
@@ -80,6 +81,10 @@ downloads list of objects List of ticket
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
|
||||
deprecated in favour of the new ``fees`` attribute but will still be served and removed in 1.9.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
|
||||
The attribute ``invoice_address.internal_reference`` has been added.
|
||||
|
||||
Order position resource
|
||||
-----------------------
|
||||
@@ -141,7 +146,7 @@ Order endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -170,6 +175,7 @@ Order endpoints
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
},
|
||||
@@ -251,7 +257,7 @@ Order endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
@@ -275,6 +281,7 @@ Order endpoints
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
},
|
||||
@@ -329,6 +336,7 @@ Order endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
|
||||
|
||||
@@ -367,9 +375,206 @@ Order endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||
**or** downlodas are not available for this order at this time. The response content will
|
||||
contain more details.
|
||||
:statuscode 404: The requested order or output provider does not exist.
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting vor a few
|
||||
seconds.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
|
||||
|
||||
Marks a pending or expired order as successfully paid.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_paid/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "p",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be marked as paid, either because the current order status does not allow it or because no quota is left to perform the operation.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_canceled/
|
||||
|
||||
Marks a pending order as canceled.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_canceled/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"send_email": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "c",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be marked as canceled since the current order status does not allow it.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||
|
||||
Marks a paid order as unpaid.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_pending/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "n",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be marked as unpaid since the current order status does not allow it.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_expired/
|
||||
|
||||
Marks a unpaid order as expired.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_expired/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "e",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be marked as expired since the current order status does not allow it.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/extend/
|
||||
|
||||
Extends the payment deadline of a pending order. If the order is already expired and quota is still
|
||||
available, its state will be changed to pending.
|
||||
|
||||
The only required parameter of this operation is ``expires``, which should contain a date in the future.
|
||||
Note that only a date is expected, not a datetime, since pretix will always set the deadline to the end of the
|
||||
day in the event's timezone.
|
||||
|
||||
You can pass the optional parameter ``force``. If it is set to ``true``, the operation will be performed even if
|
||||
it leads to an overbooked quota because the order was expired and the tickets have been sold again.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/extend/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"expires": "2017-10-28",
|
||||
"force": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "n",
|
||||
"expires": "2017-10-28T23:59:59Z",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be extended since the current order status does not allow it or no quota is available or the submitted date is invalid.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
|
||||
Order position endpoints
|
||||
------------------------
|
||||
@@ -392,7 +597,7 @@ Order position endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -476,7 +681,7 @@ Order position endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 23442,
|
||||
@@ -520,6 +725,7 @@ Order position endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position does not exist.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
|
||||
|
||||
@@ -559,5 +765,6 @@ Order position endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||
**or** downlodas are not available for this order position at this time. The response content will
|
||||
contain more details.
|
||||
:statuscode 404: The requested order position or download provider does not exist.
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting vor a few
|
||||
seconds.
|
||||
|
||||
@@ -41,7 +41,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -77,7 +77,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Big Events LLC",
|
||||
|
||||
@@ -54,7 +54,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -113,7 +113,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -42,7 +42,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -88,7 +88,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
@@ -124,7 +124,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"available": true,
|
||||
|
||||
@@ -60,7 +60,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -114,7 +114,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -25,6 +25,10 @@ home_country string Merchant countr
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -47,7 +51,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -90,7 +94,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
@@ -103,7 +107,128 @@ Endpoints
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``slug`` field of the sub-event to fetch
|
||||
:param id: The ``id`` field of the tax rule to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/taxrules/
|
||||
|
||||
Create a new tax rule.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/taxrules/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"name": {"en": "VAT"},
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a tax rule for
|
||||
:param event: The ``slug`` field of the event to create a tax rule for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The tax rule could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create tax rules.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/taxrules/(id)/
|
||||
|
||||
Update a tax rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/taxrules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"rate": "20.00",
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"rate": "20.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the tax rule to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The tax rule could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/taxrules/(id)/
|
||||
|
||||
Delete a tax rule. Note that tax rules can only be deleted if they are not in use for any products, settings
|
||||
or orders. If you cannot delete a tax rule, this method will return a ``403`` status code and you can only
|
||||
discontinue using it everywhere else.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/taxrules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the tax rule to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||
|
||||
@@ -44,6 +44,10 @@ subevent integer ID of the date
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -65,7 +69,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -136,7 +140,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
@@ -162,3 +166,152 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/vouchers/
|
||||
|
||||
Create a new voucher.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/vouchers/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 408
|
||||
|
||||
{
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"valid_until": null,
|
||||
"block_quota": false,
|
||||
"allow_ignore_quota": false,
|
||||
"price_mode": "set",
|
||||
"value": "12.00",
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
"valid_until": null,
|
||||
"block_quota": false,
|
||||
"allow_ignore_quota": false,
|
||||
"price_mode": "set",
|
||||
"value": "12.00",
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||
:param event: The ``slug`` field of the event to create a voucher for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The voucher could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
|
||||
|
||||
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
her.
|
||||
|
||||
You can change all fields of the resource except the ``id`` and ``redeemed`` fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/vouchers/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 408
|
||||
|
||||
{
|
||||
"price_mode": "set",
|
||||
"value": "24.00",
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
"valid_until": null,
|
||||
"block_quota": false,
|
||||
"allow_ignore_quota": false,
|
||||
"price_mode": "set",
|
||||
"value": "24.00",
|
||||
"item": 1,
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the tax rule to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The voucher could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
|
||||
|
||||
Delete a voucher. Note that you cannot delete a voucher if it already has been redeemed.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/vouchers/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the tax rule 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.
|
||||
|
||||
@@ -48,7 +48,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -102,7 +102,7 @@ Endpoints
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -60,7 +60,85 @@ your views::
|
||||
def admin_view(request, organizer, event):
|
||||
...
|
||||
|
||||
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``.
|
||||
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of
|
||||
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
|
||||
|
||||
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
||||
def navbar_info(sender, request, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
|
||||
return []
|
||||
return [{
|
||||
'label': _('My plugin view'),
|
||||
'icon': 'heart',
|
||||
'url': reverse('plugins:myplugin:index', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'review',
|
||||
}]
|
||||
|
||||
|
||||
Event settings view
|
||||
-------------------
|
||||
|
||||
A special case of a control panel view is a view hooked into the event settings page. For this case, there is a
|
||||
special navigation signal::
|
||||
|
||||
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
|
||||
def navbar_settings(sender, request, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
return [{
|
||||
'label': _('My settings'),
|
||||
'url': reverse('plugins:myplugin:settings', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'settings',
|
||||
}]
|
||||
|
||||
Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html``
|
||||
for good integration. If you just want to display a form, you could do it like the following::
|
||||
|
||||
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
||||
model = Event
|
||||
permission = 'can_change_settings'
|
||||
form_class = MySettingsForm
|
||||
template_name = 'my_plugin/settings.html'
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse('plugins:myplugin:settings', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
With this template::
|
||||
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %} {% trans "Friends Tickets Settings" %} {% endblock %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Friends Tickets Settings" %}</legend>
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
Frontend views
|
||||
--------------
|
||||
|
||||
@@ -19,13 +19,13 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, order_paid, order_placed
|
||||
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, question_form_fields, checkout_confirm_messages
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -47,11 +47,11 @@ Backend
|
||||
-------
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
|
||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, event_settings_widget
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, requiredaction_display
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
@@ -64,3 +64,9 @@ Dashboards
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: event_dashboard_widgets, user_dashboard_widgets
|
||||
|
||||
Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: layout_text_variables
|
||||
|
||||
@@ -114,6 +114,19 @@ method to make your receivers available::
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
You can optionally specify code that is executed when your plugin is activated for an event
|
||||
in the ``installed`` method::
|
||||
|
||||
class PaypalApp(AppConfig):
|
||||
…
|
||||
|
||||
def installed(self, event):
|
||||
pass # Your code here
|
||||
|
||||
|
||||
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
|
||||
because the event is created with settings copied from another event.
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ If an item is assigned to multiple quotas, it can only be bought if *all of them
|
||||
If multiple items are assigned to the same quota, the quota will be counted as sold out as soon as the
|
||||
*sum* of the two items exceeds the quota limit.
|
||||
|
||||
The availability of a quota is currently calculated by substracting the following numbers from the quota
|
||||
The availability of a quota is currently calculated by subtracting the following numbers from the quota
|
||||
limit:
|
||||
|
||||
* The number of orders placed for an item that are either already paid or within their granted payment period
|
||||
|
||||
@@ -21,7 +21,7 @@ Organizers and events
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Event
|
||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, get_cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
|
||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
|
||||
|
||||
.. autoclass:: pretix.base.models.SubEvent
|
||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
|
||||
|
||||
@@ -20,7 +20,6 @@ Your should install the following on your system:
|
||||
|
||||
* Python 3.4 or newer
|
||||
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
||||
* ``pyvenv`` for Python 3 (Debian package: ``python3-venv``)
|
||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
||||
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||
@@ -37,7 +36,7 @@ Please execute ``python -V`` or ``python3 -V`` to make sure you have Python 3.4
|
||||
execute ``pip3 -V`` to check. Then use Python's internal tools to create a virtual
|
||||
environment and activate it for your current session::
|
||||
|
||||
pyvenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
You should now see a ``(env)`` prepended to your shell prompt. You have to do this
|
||||
|
||||
BIN
doc/screens/event/widget_form.png
Normal file
BIN
doc/screens/event/widget_form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
104
doc/user/events/widget.rst
Normal file
104
doc/user/events/widget.rst
Normal file
@@ -0,0 +1,104 @@
|
||||
Embeddable Widget
|
||||
=================
|
||||
|
||||
If you want to show your ticket shop on your event website or blog, you can use our JavaScript widget. This way,
|
||||
users will not need to leave your site to buy their ticket in most cases. The widget will still open a new tab
|
||||
for the checkout if the user is on a mobile device.
|
||||
|
||||
To obtain the correct HTML code for embedding your event into your website, we recommend that you go to the "Widget"
|
||||
tab of your event's settings. You can specify some optional settings there (for example the language of the widget)
|
||||
and then click "Generate widget code".
|
||||
|
||||
.. thumbnail:: ../../screens/event/widget_form.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
|
||||
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
|
||||
|
||||
The second snippet should be embedded at the position where the widget should show up::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/"></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without JavaScript,
|
||||
please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
.. note::
|
||||
|
||||
You can of course embed multiple widgets of multiple events on your page. In this case, please add the first
|
||||
snippet only *once* and the second snippets once *for each event*.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Your embedded widget could look like the following:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/"></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
|
||||
Styling
|
||||
-------
|
||||
|
||||
If you want, you can customize the appearance of the widget to fit your website with CSS. If you inspect the rendered
|
||||
HTML of the widget with your browser's developer tools, you will see that nearly every element has a custom class
|
||||
and all classes are prefixed with ``pretix-widget``. You can override the styles as much as you want to and if
|
||||
you want to go all custom, you don't even need to use the stylesheet provided by us at all.
|
||||
|
||||
SSL
|
||||
---
|
||||
|
||||
Since buying a ticket normally involves entering sensitive data, we strongly suggest that you use SSL/HTTPS for the page
|
||||
that includes the widget. Initiatives like `Let's Encrypt`_ allow you to obtain a SSL certificat free of charge.
|
||||
|
||||
All data transferred to pretix will be made over SSL, even if using the widget on a non-SSL site. However, without
|
||||
using SSL for your site, a man-in-the-middle attacker could potentially alter the widget in dangerous ways. Moreover,
|
||||
using SSL is becoming standard practice and your customers might want expect see the secure lock icon in their browser
|
||||
granted to SSL-enabled web pages.
|
||||
|
||||
By default, the checkout process will open in a new tab in your customer's browsers if you don't use SSL for your
|
||||
website. If you confident to have a good reason for not using SSL, you can override this behaviour with the
|
||||
``skip-ssl-check`` attribute::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
|
||||
|
||||
Pre-selecting a voucher
|
||||
-----------------------
|
||||
|
||||
You can pre-select a voucher for the widget with the ``voucher`` attribute::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" voucher="ABCDE123456"></pretix-widget>
|
||||
|
||||
This way, the widget will only show products that can be bought with the voucher and prices according to the
|
||||
voucher's settings.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" voucher="ABCDE123456"></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
@@ -11,3 +11,4 @@ wanting to use pretix to sell tickets.
|
||||
events/create
|
||||
events/taxes
|
||||
payments/index
|
||||
events/widget
|
||||
|
||||
48
res/icon.svg
48
res/icon.svg
@@ -1,44 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="149.59399"
|
||||
height="149.59399"
|
||||
id="svg2"
|
||||
viewBox="0 0 149.59399 149.59399"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
sodipodi:docname="icon_draft.svg">
|
||||
id="svg2"
|
||||
height="159.56693"
|
||||
width="159.56693">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="3.959798"
|
||||
inkscape:cx="141.14985"
|
||||
inkscape:cy="82.686886"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1914"
|
||||
inkscape:window-height="1039"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="0"
|
||||
fit-margin-top="20"
|
||||
fit-margin-left="20"
|
||||
fit-margin-right="20"
|
||||
fit-margin-bottom="20" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
@@ -52,15 +25,12 @@
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-257.78125,-548.74975)">
|
||||
transform="translate(-257.78125,-548.74975)"
|
||||
id="layer1">
|
||||
<path
|
||||
style="color:#000000;fill:#3b1c4a;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
d="m 277.78125,568.74975 0,34.09383 c 11.37842,0 20.613,9.28198 20.613,20.7188 0,11.4368 -9.23458,20.68754 -20.613,20.68754 l 0,34.09383 68.33691,0 0,-9.50002 2.98469,0 0,9.50002 38.2724,0 0,-34.09383 c -0.0104,2e-5 -0.0207,0 -0.031,0 -11.37841,0 -20.613,-9.25074 -20.613,-20.68754 0,-11.43682 9.23459,-20.7188 20.613,-20.7188 0.0105,0 0.0207,-2e-5 0.031,0 l 0,-34.09383 -38.2724,0 0,9.09377 -2.98469,0 0,-9.09377 z m 68.33691,16.09379 2.98469,0 0,14.00003 -2.98469,0 z m 0,21.00004 2.98469,0 0,14.00004 -2.98469,0 z m -24.40604,3.68751 c 4.02516,3e-5 7.02244,1.10354 8.98515,3.34376 1.96268,2.20685 2.92251,5.27326 2.92251,9.21877 l 0,3.87501 c 0,3.94554 -0.95983,7.04102 -2.92251,9.28127 -1.96271,2.20683 -4.95999,3.31251 -8.98515,3.31251 -0.5988,0 -1.31361,-0.0268 -2.14524,-0.0937 -0.83167,-0.0334 -1.71126,-0.11626 -2.64269,-0.25 l 0,8.87502 c -1e-5,0.26748 -0.11132,0.48687 -0.31091,0.6875 -0.19961,0.20061 -0.41788,0.31249 -0.68399,0.3125 l -4.60139,0 c -0.26614,-1e-5 -0.4844,-0.11189 -0.684,-0.3125 -0.19959,-0.20063 -0.3109,-0.42002 -0.3109,-0.6875 l 0,-34.90633 c 0,-0.36777 0.0824,-0.64529 0.24872,-0.8125 0.16633,-0.20059 0.52265,-0.39529 1.08817,-0.5625 1.5635,-0.40121 3.24248,-0.70343 5.00557,-0.93751 1.76309,-0.23403 3.43988,-0.34372 5.03666,-0.34375 z m 0,5.40627 c -0.89819,2e-5 -1.80453,0.0269 -2.73596,0.0937 -0.89819,0.0669 -1.58626,0.14971 -2.05197,0.25 l 0,17.50004 c 0.69857,0.10031 1.49359,0.18313 2.42505,0.25 0.9647,0.0669 1.76408,0.12501 2.36288,0.125 1.06449,10e-6 1.94409,-0.19469 2.64269,-0.5625 0.69857,-0.36781 1.24859,-0.86471 1.6478,-1.50001 0.39917,-0.63529 0.67527,-1.38064 0.80835,-2.25 0.16631,-0.86934 0.24871,-1.83846 0.24873,-2.87501 l 0,-3.87501 c -2e-5,-1.03651 -0.0824,-1.97438 -0.24873,-2.84375 -0.13308,-0.86934 -0.40918,-1.61469 -0.80835,-2.25001 -0.39921,-0.63527 -0.94923,-1.13218 -1.6478,-1.5 -0.6986,-0.36778 -1.5782,-0.56248 -2.64269,-0.5625 z m 24.40604,11.90627 2.98469,0 0,14.00003 -2.98469,0 z m 0,21.00005 2.98469,0 0,14.00003 -2.98469,0 z"
|
||||
id="rect3888"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccsccccccccssscccccccccccccccccccsscscccccccssccccccccccccccccccccccccccccc" />
|
||||
transform="matrix(0.93749999,0,0,0.93749999,257.78125,548.74975)"
|
||||
d="M 21.333984 21.333984 L 21.333984 57.699219 C 33.470966 57.699219 43.320312 67.601506 43.320312 79.800781 C 43.320312 92.000035 33.470966 101.86719 21.333984 101.86719 L 21.333984 138.23438 L 94.226562 138.23438 L 94.226562 128.09961 L 97.410156 128.09961 L 97.410156 138.23438 L 138.23438 138.23438 L 138.23438 101.86719 C 138.22328 101.86721 138.21216 101.86719 138.20117 101.86719 C 126.0642 101.86719 116.21289 92.000035 116.21289 79.800781 C 116.21289 67.601506 126.0642 57.699219 138.20117 57.699219 C 138.21237 57.699219 138.22339 57.699197 138.23438 57.699219 L 138.23438 21.333984 L 97.410156 21.333984 L 97.410156 31.033203 L 94.226562 31.033203 L 94.226562 21.333984 L 21.333984 21.333984 z M 94.226562 38.5 L 97.410156 38.5 L 97.410156 53.433594 L 94.226562 53.433594 L 94.226562 38.5 z M 94.226562 60.900391 L 97.410156 60.900391 L 97.410156 75.833984 L 94.226562 75.833984 L 94.226562 60.900391 z M 67.044922 64.027344 C 76.359333 64.027344 82.662109 68.991742 82.662109 79.533203 C 82.662109 89.014942 77.139434 95.039062 69.386719 95.039062 C 67.490377 95.039062 65.927327 94.814901 65.146484 94.591797 L 65.146484 106.64062 L 54.550781 106.64062 L 54.550781 66.314453 C 57.395304 64.97585 61.244324 64.027344 67.044922 64.027344 z M 66.990234 70.216797 C 66.209392 70.216797 65.648458 70.328766 65.146484 70.496094 L 65.146484 88.568359 C 65.536906 88.735677 66.097199 88.845703 66.822266 88.845703 C 70.61497 88.845703 72.175781 85.725087 72.175781 79.589844 C 72.175781 73.287273 70.838704 70.216797 66.990234 70.216797 z M 94.226562 83.300781 L 97.410156 83.300781 L 97.410156 98.234375 L 94.226562 98.234375 L 94.226562 83.300781 z M 94.226562 105.69922 L 97.410156 105.69922 L 97.410156 120.63281 L 94.226562 120.63281 L 94.226562 105.69922 z "
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#3b1c4a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.06666672;marker:none;enable-background:accumulate" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -6,7 +6,7 @@ localecompile:
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --all --ignore "pretix/helpers/*"
|
||||
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "build/*"
|
||||
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*"
|
||||
|
||||
staticfiles: jsi18n
|
||||
./manage.py collectstatic --noinput
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.9.0"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -13,6 +14,13 @@ class EventPermission(BasePermission):
|
||||
return True
|
||||
return False
|
||||
|
||||
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
||||
required_permission = getattr(view, 'write_permission')
|
||||
elif hasattr(view, 'permission'):
|
||||
required_permission = getattr(view, 'permission')
|
||||
else:
|
||||
required_permission = None
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
||||
else request.user)
|
||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||
@@ -25,9 +33,8 @@ class EventPermission(BasePermission):
|
||||
request.organizer = request.event.organizer
|
||||
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
|
||||
|
||||
if hasattr(view, 'permission'):
|
||||
if view.permission and view.permission not in request.eventpermset:
|
||||
return False
|
||||
if required_permission and required_permission not in request.eventpermset:
|
||||
return False
|
||||
|
||||
elif 'organizer' in request.resolver_match.kwargs:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
@@ -37,7 +44,21 @@ class EventPermission(BasePermission):
|
||||
return False
|
||||
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
|
||||
|
||||
if hasattr(view, 'permission'):
|
||||
if view.permission and view.permission not in request.orgapermset:
|
||||
return False
|
||||
if required_permission and required_permission not in request.orgapermset:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def permission_required(required_permission):
|
||||
def decorator(function):
|
||||
def wrapper(self, request, *args, **kw):
|
||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||
if required_permission and required_permission not in request.eventpermset:
|
||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
||||
elif 'organizer' in request.resolver_match.kwargs:
|
||||
if required_permission and required_permission not in request.orgapermset:
|
||||
raise PermissionDenied('You do not have permission to perform this operation.')
|
||||
|
||||
return function(self, request, *args, **kw)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
16
src/pretix/api/exception.py
Normal file
16
src/pretix/api/exception.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import exception_handler, status
|
||||
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
|
||||
if isinstance(exc, LockTimeoutException):
|
||||
response = Response(
|
||||
{'detail': 'The server was too busy to process your request. Please try again.'},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.conf import settings
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
||||
@@ -22,6 +24,16 @@ class I18nField(Field):
|
||||
settings.LANGUAGE_CODE: str(value.data)
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str):
|
||||
return LazyI18nString(data)
|
||||
elif isinstance(data, dict):
|
||||
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
|
||||
raise ValidationError('Invalid languages included.')
|
||||
return LazyI18nString(data)
|
||||
else:
|
||||
raise ValidationError('Invalid data type.')
|
||||
|
||||
|
||||
class I18nAwareModelSerializer(ModelSerializer):
|
||||
pass
|
||||
|
||||
@@ -26,7 +26,7 @@ class InvoiceAdddressSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'vat_id_validated')
|
||||
'vat_id_validated', 'internal_reference')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
@@ -153,4 +153,5 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
model = Invoice
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date')
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||
'internal_reference')
|
||||
|
||||
@@ -8,3 +8,36 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent')
|
||||
read_only_fields = ('id', 'redeemed')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
Voucher.clean_item_properties(
|
||||
full_data, self.context.get('event'),
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
|
||||
)
|
||||
Voucher.clean_subevent(
|
||||
full_data, self.context.get('event')
|
||||
)
|
||||
Voucher.clean_max_usages(full_data, self.instance.redeemed if self.instance else 0)
|
||||
check_quota = Voucher.clean_quota_needs_checking(
|
||||
full_data, self.instance,
|
||||
item_changed=self.instance and (
|
||||
full_data.get('item') != self.instance.item or
|
||||
full_data.get('variation') != self.instance.variation or
|
||||
full_data.get('quota') != self.instance.quota
|
||||
),
|
||||
creating=not self.instance
|
||||
)
|
||||
if check_quota:
|
||||
Voucher.clean_quota_check(
|
||||
full_data, 1, self.instance, self.context.get('event'),
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
|
||||
)
|
||||
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from pretix.api.serializers.event import (
|
||||
EventSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.base.models import Event, ItemCategory, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -36,9 +38,39 @@ class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class TaxRuleViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class TaxRuleViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = TaxRuleSerializer
|
||||
queryset = TaxRule.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.tax_rules.all()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.taxrule.changed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.taxrule.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
|
||||
|
||||
instance.log_action(
|
||||
'pretix.event.taxrule.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@@ -34,6 +34,7 @@ class ItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
filter_class = ItemFilter
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
|
||||
@@ -52,6 +53,7 @@ class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_class = ItemCategoryFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.categories.all()
|
||||
@@ -63,6 +65,7 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = (OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
@@ -81,6 +84,7 @@ class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_class = QuotaFilter
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.quotas.all()
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import datetime
|
||||
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import FileResponse
|
||||
from django.utils.timezone import make_aware
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import serializers, status, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
|
||||
)
|
||||
from pretix.base.models import Invoice, Order, OrderPosition
|
||||
from pretix.base.models import Invoice, Order, OrderPosition, Quota
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.invoices import invoice_pdf
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderError, cancel_order, extend_order, mark_order_paid,
|
||||
)
|
||||
from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
)
|
||||
@@ -34,6 +44,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_class = OrderFilter
|
||||
lookup_field = 'code'
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.orders.prefetch_related(
|
||||
@@ -71,6 +82,130 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
return resp
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
try:
|
||||
mark_order_paid(
|
||||
order, manual=True,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except SendMailException:
|
||||
pass
|
||||
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
return Response(
|
||||
{'detail': 'The order is not pending or expired.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_canceled(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
return Response(
|
||||
{'detail': 'The order is not pending.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
cancel_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
send_mail=send_mail
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_pending(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status != Order.STATUS_PAID:
|
||||
return Response(
|
||||
{'detail': 'The order is not paid.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.payment_manual = True
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.unpaid',
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_expired(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
return Response(
|
||||
{'detail': 'The order is not pending.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expired',
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
# TODO: Find a way to implement mark_refunded
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def extend(self, request, **kwargs):
|
||||
new_date = request.data.get('expires', None)
|
||||
force = request.data.get('force', False)
|
||||
if not new_date:
|
||||
return Response(
|
||||
{'detail': 'New date is missing.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
df = serializers.DateField()
|
||||
try:
|
||||
new_date = df.to_internal_value(new_date)
|
||||
except:
|
||||
return Response(
|
||||
{'detail': 'New date is invalid.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
tz = pytz.timezone(self.request.event.settings.timezone)
|
||||
new_date = make_aware(datetime.datetime.combine(
|
||||
new_date,
|
||||
datetime.time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
|
||||
order = self.get_object()
|
||||
|
||||
try:
|
||||
extend_order(
|
||||
order,
|
||||
new_date=new_date,
|
||||
force=force,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
except OrderError as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
||||
|
||||
@@ -4,10 +4,12 @@ from django_filters.rest_framework import (
|
||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||
)
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
|
||||
|
||||
class VoucherFilter(FilterSet):
|
||||
@@ -27,7 +29,7 @@ class VoucherFilter(FilterSet):
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
|
||||
|
||||
class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class VoucherViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = VoucherSerializer
|
||||
queryset = Voucher.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -35,6 +37,49 @@ class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
|
||||
filter_class = VoucherFilter
|
||||
permission = 'can_view_vouchers'
|
||||
write_permission = 'can_change_vouchers'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.vouchers.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
with request.event.lock():
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.voucher.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
with request.event.lock():
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.voucher.changed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This voucher can not be deleted as it has already been used.')
|
||||
|
||||
instance.log_action(
|
||||
'pretix.voucher.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@@ -11,7 +11,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import payment # NOQA
|
||||
from . import exporters # NOQA
|
||||
from . import invoice # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import hashlib
|
||||
import time
|
||||
from typing import Dict, List
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.db.models import Model
|
||||
@@ -11,17 +11,19 @@ class NamespacedCache:
|
||||
def __init__(self, prefixkey: str, cache: str='default'):
|
||||
self.cache = caches[cache]
|
||||
self.prefixkey = prefixkey
|
||||
self._last_prefix = None
|
||||
|
||||
def _prefix_key(self, original_key: str) -> str:
|
||||
def _prefix_key(self, original_key: str, known_prefix=None) -> str:
|
||||
# Race conditions can happen here, but should be very very rare.
|
||||
# We could only handle this by going _really_ lowlevel using
|
||||
# memcached's `add` keyword instead of `set`.
|
||||
# See also:
|
||||
# https://code.google.com/p/memcached/wiki/NewProgrammingTricks#Namespacing
|
||||
prefix = self.cache.get(self.prefixkey)
|
||||
prefix = known_prefix or self.cache.get(self.prefixkey)
|
||||
if prefix is None:
|
||||
prefix = int(time.time())
|
||||
self.cache.set(self.prefixkey, prefix)
|
||||
self._last_prefix = prefix
|
||||
key = '%s:%d:%s' % (self.prefixkey, prefix, original_key)
|
||||
if len(key) > 200: # Hash long keys, as memcached has a length limit
|
||||
# TODO: Use a more efficient, non-cryptographic hash algorithm
|
||||
@@ -32,17 +34,25 @@ class NamespacedCache:
|
||||
return key.split(":", 2 + self.prefixkey.count(":"))[-1]
|
||||
|
||||
def clear(self) -> None:
|
||||
self._last_prefix = None
|
||||
try:
|
||||
prefix = self.cache.incr(self.prefixkey, 1)
|
||||
except ValueError:
|
||||
prefix = int(time.time())
|
||||
self.cache.set(self.prefixkey, prefix)
|
||||
|
||||
def set(self, key: str, value: str, timeout: int=3600):
|
||||
def set(self, key: str, value: str, timeout: int=300):
|
||||
return self.cache.set(self._prefix_key(key), value, timeout)
|
||||
|
||||
def get(self, key: str) -> str:
|
||||
return self.cache.get(self._prefix_key(key))
|
||||
return self.cache.get(self._prefix_key(key, known_prefix=self._last_prefix))
|
||||
|
||||
def get_or_set(self, key: str, default: Callable, timeout=300) -> str:
|
||||
return self.cache.get_or_set(
|
||||
self._prefix_key(key, known_prefix=self._last_prefix),
|
||||
default=default,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
def get_many(self, keys: List[str]) -> Dict[str, str]:
|
||||
values = self.cache.get_many([self._prefix_key(key) for key in keys])
|
||||
@@ -51,7 +61,7 @@ class NamespacedCache:
|
||||
newvalues[self._strip_prefix(k)] = v
|
||||
return newvalues
|
||||
|
||||
def set_many(self, values: Dict[str, str], timeout=3600):
|
||||
def set_many(self, values: Dict[str, str], timeout=300):
|
||||
newvalues = {}
|
||||
for k, v in values.items():
|
||||
newvalues[self._prefix_key(k)] = v
|
||||
|
||||
@@ -25,7 +25,9 @@ class AnswerFilesExporter(BaseExporter):
|
||||
forms.ModelMultipleChoiceField(
|
||||
queryset=self.event.questions.filter(type='F'),
|
||||
label=_('Questions'),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
widget=forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
@@ -41,7 +43,7 @@ class AnswerFilesExporter(BaseExporter):
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for i in qs:
|
||||
if i.file:
|
||||
i.file.open('r')
|
||||
i.file.open('rb')
|
||||
fname = '{}-{}-{}-q{}-{}'.format(
|
||||
self.event.slug.upper(),
|
||||
i.orderposition.order.code,
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.contrib.auth.password_validation import (
|
||||
)
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.base.models import User
|
||||
|
||||
@@ -31,17 +32,19 @@ class UserSettingsForm(forms.ModelForm):
|
||||
required=False,
|
||||
label=_("Repeat new password"),
|
||||
widget=forms.PasswordInput())
|
||||
# timezone = forms.ChoiceField(
|
||||
# choices=((a, a) for a in common_timezones),
|
||||
# label=_("Default timezone"),
|
||||
# )
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
label=_("Default timezone"),
|
||||
help_text=_('Only used for views that are not bound to an event. For all '
|
||||
'event views, the event timezone is used instead.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'fullname',
|
||||
'locale',
|
||||
# 'timezone',
|
||||
'timezone',
|
||||
'email'
|
||||
]
|
||||
|
||||
|
||||
@@ -331,6 +331,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Your reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.introductory_text:
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
@@ -47,10 +47,10 @@ class LocaleMiddleware(MiddlewareMixin):
|
||||
request.LANGUAGE_CODE = translation.get_language()
|
||||
|
||||
tzname = None
|
||||
if request.user.is_authenticated:
|
||||
tzname = request.user.timezone
|
||||
if hasattr(request, 'event'):
|
||||
tzname = request.event.settings.timezone
|
||||
elif request.user.is_authenticated:
|
||||
tzname = request.user.timezone
|
||||
if tzname:
|
||||
try:
|
||||
timezone.activate(pytz.timezone(tzname))
|
||||
@@ -186,11 +186,13 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'style-src': ["{static}", "{media}", "'nonce-{nonce}'"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
'font-src': ["{static}"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||
# this. However, we'll restrict it to HTTPS.
|
||||
'form-action': ["{dynamic}", "https:"],
|
||||
'report-uri': ["/csp_report/"],
|
||||
}
|
||||
if 'Content-Security-Policy' in resp:
|
||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||
@@ -218,7 +220,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
dynamicdomain += " " + domain
|
||||
|
||||
if request.path not in self.CSP_EXEMPT:
|
||||
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain, nonce=request.csp_nonce)
|
||||
for k, v in h.items():
|
||||
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain,
|
||||
nonce=request.csp_nonce).split(' ')
|
||||
resp['Content-Security-Policy'] = _render_csp(h)
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
return resp
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-11-03 11:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.migrations.operations.special
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def fee_converter(app, schema_editor):
|
||||
OrderFee = app.get_model('pretixbase', 'OrderFee')
|
||||
Order = app.get_model('pretixbase', 'Order')
|
||||
|
||||
of = []
|
||||
for o in Order.objects.exclude(payment_fee=Decimal('0.00')).iterator():
|
||||
of.append(OrderFee(
|
||||
order=o,
|
||||
value=o.payment_fee,
|
||||
fee_type='payment',
|
||||
tax_rate=o.payment_fee_tax_rate,
|
||||
tax_rule=o.payment_fee_tax_rule,
|
||||
tax_value=o.payment_fee_tax_value,
|
||||
internal_type=o.payment_provider
|
||||
))
|
||||
if len(of) > 900:
|
||||
OrderFee.objects.bulk_create(of)
|
||||
of = []
|
||||
OrderFee.objects.bulk_create(of)
|
||||
|
||||
|
||||
def assign_positions(app, schema_editor):
|
||||
Invoice = app.get_model('pretixbase', 'Invoice')
|
||||
|
||||
for i in Invoice.objects.iterator():
|
||||
for j, l in enumerate(i.lines.all()):
|
||||
l.position = j
|
||||
l.save()
|
||||
|
||||
|
||||
def clear_quota_caches(app, schema_editor):
|
||||
Quota = app.get_model('pretixbase', 'Quota')
|
||||
Quota.objects.all().update(cached_availability_time=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('pretixbase', '0076_orderfee'), ('pretixbase', '0077_auto_20170829_1126'), ('pretixbase', '0078_auto_20171003_1650'), ('pretixbase', '0079_auto_20171010_2117'), ('pretixbase', '0080_auto_20171016_1553'), ('pretixbase', '0081_quota_cached_availability_paid_orders'), ('pretixbase', '0082_invoiceaddress_internal_reference')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0075_auto_20170828_0901'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderFee',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Value')),
|
||||
('description', models.CharField(blank=True, max_length=190)),
|
||||
('internal_type', models.CharField(blank=True, max_length=255)),
|
||||
('fee_type', models.CharField(choices=[('payment', 'Payment method fee'), ('shipping', 'Shipping fee')], max_length=100)),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Tax rate')),
|
||||
('tax_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax value')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fees', to='pretixbase.Order', verbose_name='Order')),
|
||||
('tax_rule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TaxRule')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=fee_converter,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rule',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_value',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='fee_type',
|
||||
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('other', 'Other fees')], max_length=100),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=assign_positions,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='invoiceline',
|
||||
options={'ordering': ('position', 'pk')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_number',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_state',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_time',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
name='default',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='eu_reverse_charge',
|
||||
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='api_token',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TeamAPIToken'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='name',
|
||||
field=i18nfield.fields.I18nCharField(max_length=200, verbose_name='Event name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, help_text='If you have many products, you can optionally sort them into categories to keep things organized.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.ItemCategory', verbose_name='Category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='cart_id',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_paid_orders',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=clear_quota_caches,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='internal_reference',
|
||||
field=models.TextField(blank=True, help_text='This reference will be printed on your invoice for your convenience.', verbose_name='Internal reference'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='internal_reference',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
40
src/pretix/base/migrations/0078_auto_20171003_1650.py
Normal file
40
src/pretix/base/migrations/0078_auto_20171003_1650.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-10-03 16:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0077_auto_20170829_1126'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_number',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_state',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_time',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
name='default',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='eu_reverse_charge',
|
||||
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
|
||||
),
|
||||
]
|
||||
32
src/pretix/base/migrations/0079_auto_20171010_2117.py
Normal file
32
src/pretix/base/migrations/0079_auto_20171010_2117.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-10-10 21:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0078_auto_20171003_1650'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='api_token',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TeamAPIToken'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='name',
|
||||
field=i18nfield.fields.I18nCharField(max_length=200, verbose_name='Event name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, help_text='If you have many products, you can optionally sort them into categories to keep things organized.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.ItemCategory', verbose_name='Category'),
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0080_auto_20171016_1553.py
Normal file
20
src/pretix/base/migrations/0080_auto_20171016_1553.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-10-16 15:53
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0079_auto_20171010_2117'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='cart_id',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-10-18 09:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def clear_quota_caches(app, schema_editor):
|
||||
Quota = app.get_model('pretixbase', 'Quota')
|
||||
Quota.objects.all().update(cached_availability_time=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0080_auto_20171016_1553'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_paid_orders',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
clear_quota_caches, migrations.RunPython.noop
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-10-26 22:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0081_quota_cached_availability_paid_orders'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='internal_reference',
|
||||
field=models.TextField(blank=True, help_text='This reference will be printed on your invoice for your convenience.', verbose_name='Internal reference'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='internal_reference',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -3,8 +3,8 @@ from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction, SubEvent,
|
||||
generate_invite_token,
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
|
||||
@@ -251,6 +251,24 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
def get_events_with_permission(self, permission):
|
||||
"""
|
||||
Returns a queryset of events the user has a specific permissions to.
|
||||
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
return Event.objects.all()
|
||||
|
||||
kwargs = {permission: True}
|
||||
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=self.teams.filter(all_events=True, **kwargs).values_list('organizer', flat=True))
|
||||
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
|
||||
|
||||
class LoggingMixin:
|
||||
|
||||
def log_action(self, action, data=None, user=None):
|
||||
def log_action(self, action, data=None, user=None, api_token=None):
|
||||
"""
|
||||
Create a LogEntry object that is related to this object.
|
||||
See the LogEntry documentation for details.
|
||||
@@ -53,10 +53,12 @@ class LoggingMixin:
|
||||
event = self
|
||||
elif hasattr(self, 'event'):
|
||||
event = self.event
|
||||
l = LogEntry(content_object=self, user=user, action_type=action, event=event)
|
||||
if user and not user.is_authenticated:
|
||||
user = None
|
||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
|
||||
if data:
|
||||
l.data = json.dumps(data, cls=CustomJSONEncoder)
|
||||
l.save()
|
||||
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
|
||||
logentry.save()
|
||||
|
||||
|
||||
class LoggedModel(models.Model, LoggingMixin):
|
||||
|
||||
@@ -38,6 +38,31 @@ class EventMixin:
|
||||
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
|
||||
super().clean()
|
||||
|
||||
def get_short_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_short_date_to_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
if ``show_date_to`` is ``False``.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
self.date_to.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
@@ -169,7 +194,7 @@ class Event(EventMixin, LoggedModel):
|
||||
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
|
||||
name = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name"),
|
||||
verbose_name=_("Event name"),
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=50, db_index=True,
|
||||
@@ -239,7 +264,7 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.get_cache().clear()
|
||||
self.cache.clear()
|
||||
return obj
|
||||
|
||||
def get_plugins(self) -> "list[str]":
|
||||
@@ -256,6 +281,19 @@ class Event(EventMixin, LoggedModel):
|
||||
Django's built-in cache backends, but puts you into an isolated environment for
|
||||
this event, so you don't have to prefix your cache keys. In addition, the cache
|
||||
is being cleared every time the event or one of its related objects change.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
Use the property ``cache`` instead.
|
||||
"""
|
||||
return self.cache
|
||||
|
||||
@cached_property
|
||||
def cache(self):
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
Django's built-in cache backends, but puts you into an isolated environment for
|
||||
this event, so you don't have to prefix your cache keys. In addition, the cache
|
||||
is being cleared every time the event or one of its related objects change.
|
||||
"""
|
||||
from pretix.base.cache import ObjectRelatedCache
|
||||
|
||||
@@ -553,6 +591,16 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
@@ -619,7 +667,7 @@ class EventMetaProperty(LoggedModel):
|
||||
name = models.CharField(
|
||||
max_length=50, db_index=True,
|
||||
help_text=_(
|
||||
"Can not contain spaces or special characters execpt underscores"
|
||||
"Can not contain spaces or special characters except underscores"
|
||||
),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
@@ -652,6 +700,16 @@ class EventMetaValue(LoggedModel):
|
||||
class Meta:
|
||||
unique_together = ('event', 'property')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
|
||||
class SubEventMetaValue(LoggedModel):
|
||||
"""
|
||||
@@ -672,3 +730,13 @@ class SubEventMetaValue(LoggedModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = ('subevent', 'property')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
@@ -81,6 +81,7 @@ class Invoice(models.Model):
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
||||
internal_reference = models.TextField(blank=True)
|
||||
|
||||
@staticmethod
|
||||
def _to_numeric_invoice_number(number):
|
||||
|
||||
@@ -66,12 +66,12 @@ class ItemCategory(LoggedModel):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
@@ -104,6 +104,16 @@ class SubEventItem(models.Model):
|
||||
item = models.ForeignKey('Item', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
|
||||
class SubEventItemVariation(models.Model):
|
||||
"""
|
||||
@@ -121,6 +131,16 @@ class SubEventItemVariation(models.Model):
|
||||
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
|
||||
class Item(LoggedModel):
|
||||
"""
|
||||
@@ -175,6 +195,7 @@ class Item(LoggedModel):
|
||||
related_name="items",
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Category"),
|
||||
help_text=_("If you have many products, you can optionally sort them into categories to keep things organized.")
|
||||
)
|
||||
name = I18nCharField(
|
||||
max_length=255,
|
||||
@@ -289,12 +310,12 @@ class Item(LoggedModel):
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
def tax(self, price=None, base_price_is='auto'):
|
||||
price = price if price is not None else self.default_price
|
||||
@@ -417,12 +438,12 @@ class ItemVariation(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.item:
|
||||
self.item.event.get_cache().clear()
|
||||
self.item.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.item:
|
||||
self.item.event.get_cache().clear()
|
||||
self.item.event.cache.clear()
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
|
||||
"""
|
||||
@@ -594,12 +615,12 @@ class Question(LoggedModel):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
@@ -704,6 +725,10 @@ class Quota(LoggedModel):
|
||||
blank=True,
|
||||
verbose_name=_("Variations")
|
||||
)
|
||||
cached_availability_state = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_number = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Quota")
|
||||
@@ -715,14 +740,27 @@ class Quota(LoggedModel):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
clear_cache = kwargs.pop('clear_cache', True)
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
if self.event and clear_cache:
|
||||
self.event.cache.clear()
|
||||
|
||||
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
|
||||
def rebuild_cache(self, now_dt=None):
|
||||
self.cached_availability_time = None
|
||||
self.cached_availability_number = None
|
||||
self.cached_availability_state = None
|
||||
self.availability(now_dt=now_dt)
|
||||
|
||||
def cache_is_hot(self, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
return self.cached_availability_time and (now_dt - self.cached_availability_time).total_seconds() < 120
|
||||
|
||||
def availability(
|
||||
self, now_dt: datetime=None, count_waitinglist=True, _cache=None, allow_cache=False
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
This method is used to determine whether Items or ItemVariations belonging
|
||||
to this quota should currently be available for sale.
|
||||
@@ -730,12 +768,32 @@ class Quota(LoggedModel):
|
||||
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
|
||||
and the second is the number of available tickets.
|
||||
"""
|
||||
if allow_cache and self.cache_is_hot() and count_waitinglist:
|
||||
return self.cached_availability_state, self.cached_availability_number
|
||||
|
||||
if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True):
|
||||
_cache.clear()
|
||||
|
||||
if _cache is not None and self.pk in _cache:
|
||||
return _cache[self.pk]
|
||||
now_dt = now_dt or now()
|
||||
res = self._availability(now_dt, count_waitinglist)
|
||||
|
||||
self.event.cache.delete('item_quota_cache')
|
||||
if count_waitinglist and not self.cache_is_hot(now_dt):
|
||||
self.cached_availability_state = res[0]
|
||||
self.cached_availability_number = res[1]
|
||||
self.cached_availability_time = now_dt
|
||||
if self.size is None:
|
||||
self.cached_availability_paid_orders = self.count_pending_orders()
|
||||
self.save(
|
||||
update_fields=[
|
||||
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||
'cached_availability_paid_orders'
|
||||
],
|
||||
clear_cache=False
|
||||
)
|
||||
|
||||
if _cache is not None:
|
||||
_cache[self.pk] = res
|
||||
_cache['_count_waitinglist'] = count_waitinglist
|
||||
@@ -747,8 +805,9 @@ class Quota(LoggedModel):
|
||||
if size_left is None:
|
||||
return Quota.AVAILABILITY_OK, None
|
||||
|
||||
# TODO: Test for interference with old versions of Item-Quota-relations, etc.
|
||||
size_left -= self.count_paid_orders()
|
||||
paid_orders = self.count_paid_orders()
|
||||
self.cached_availability_paid_orders = paid_orders
|
||||
size_left -= paid_orders
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_GONE, 0
|
||||
|
||||
@@ -808,7 +867,7 @@ class Quota(LoggedModel):
|
||||
& Q(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gte=now_dt))
|
||||
) &
|
||||
self._position_lookup
|
||||
).values('id').distinct().count()
|
||||
).count()
|
||||
|
||||
def count_pending_orders(self) -> dict:
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
@@ -816,23 +875,23 @@ class Quota(LoggedModel):
|
||||
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
).count()
|
||||
|
||||
def count_paid_orders(self):
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
).count()
|
||||
|
||||
@cached_property
|
||||
def _position_lookup(self) -> Q:
|
||||
return (
|
||||
( # Orders for items which do not have any variations
|
||||
Q(variation__isnull=True) &
|
||||
Q(item__quotas=self)
|
||||
Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.pk).values_list('item_id', flat=True))
|
||||
) | ( # Orders for items which do have any variations
|
||||
Q(variation__quotas=self)
|
||||
Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.pk).values_list('itemvariation_id', flat=True))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import logentry_object_link
|
||||
|
||||
|
||||
class LogEntry(models.Model):
|
||||
"""
|
||||
@@ -33,6 +35,7 @@ class LogEntry(models.Model):
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
|
||||
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE)
|
||||
action_type = models.CharField(max_length=255)
|
||||
data = models.TextField(default='{}')
|
||||
@@ -146,6 +149,9 @@ class LogEntry(models.Model):
|
||||
elif a_text:
|
||||
return a_text
|
||||
else:
|
||||
for receiver, response in logentry_object_link.send(self.event, logentry=self):
|
||||
if response:
|
||||
return response
|
||||
return ''
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -314,7 +314,7 @@ class Order(LoggedModel):
|
||||
), tz)
|
||||
return term_last
|
||||
|
||||
def _can_be_paid(self) -> Union[bool, str]:
|
||||
def _can_be_paid(self, count_waitinglist=True) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
"payment settings is over."),
|
||||
@@ -331,9 +331,9 @@ class Order(LoggedModel):
|
||||
if not self.event.settings.get('payment_term_accept_late'):
|
||||
return error_messages['late']
|
||||
|
||||
return self._is_still_available()
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist)
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
}
|
||||
@@ -351,7 +351,7 @@ class Order(LoggedModel):
|
||||
for quota in quotas:
|
||||
if quota.id not in quota_cache:
|
||||
quota_cache[quota.id] = quota
|
||||
quota.cached_availability = quota.availability(now_dt)[1]
|
||||
quota.cached_availability = quota.availability(now_dt, count_waitinglist=count_waitinglist)[1]
|
||||
else:
|
||||
# Use cached version
|
||||
quota = quota_cache[quota.id]
|
||||
@@ -728,10 +728,6 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
||||
|
||||
class Meta:
|
||||
@@ -832,7 +828,7 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Event")
|
||||
)
|
||||
cart_id = models.CharField(
|
||||
max_length=255, null=True, blank=True,
|
||||
max_length=255, null=True, blank=True, db_index=True,
|
||||
verbose_name=_("Cart ID (e.g. session key)")
|
||||
)
|
||||
datetime = models.DateTimeField(
|
||||
@@ -885,6 +881,11 @@ class InvoiceAddress(models.Model):
|
||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||
help_text=_('Only for business customers within the EU.'))
|
||||
vat_id_validated = models.BooleanField(default=False)
|
||||
internal_reference = models.TextField(
|
||||
verbose_name=_('Internal reference'),
|
||||
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
|
||||
@@ -3,6 +3,7 @@ import string
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
@@ -62,6 +63,19 @@ class Organizer(LoggedModel):
|
||||
Django's built-in cache backends, but puts you into an isolated environment for
|
||||
this organizer, so you don't have to prefix your cache keys. In addition, the cache
|
||||
is being cleared every time the organizer changes.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
Use the property ``cache`` instead.
|
||||
"""
|
||||
return self.cache
|
||||
|
||||
@cached_property
|
||||
def cache(self):
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
Django's built-in cache backends, but puts you into an isolated environment for
|
||||
this organizer, so you don't have to prefix your cache keys. In addition, the cache
|
||||
is being cleared every time the organizer changes.
|
||||
"""
|
||||
from pretix.base.cache import ObjectRelatedCache
|
||||
|
||||
|
||||
@@ -81,6 +81,16 @@ class TaxRule(LoggedModel):
|
||||
'if configured above.'),
|
||||
)
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderFee, OrderPosition
|
||||
|
||||
return (
|
||||
not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
|
||||
and not OrderPosition.objects.filter(tax_rule=self, order__event=self.event).exists()
|
||||
and not self.event.items.filter(tax_rule=self).exists()
|
||||
and self.event.settings.tax_rate_default != self
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def zero(cls):
|
||||
return cls(
|
||||
@@ -172,3 +182,13 @@ class TaxRule(LoggedModel):
|
||||
|
||||
# Consumer in different EU country / invalid VAT
|
||||
return True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
@@ -180,33 +181,149 @@ class Voucher(LoggedModel):
|
||||
def __str__(self):
|
||||
return self.code
|
||||
|
||||
def allow_delete(self):
|
||||
return self.redeemed == 0
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.quota:
|
||||
if self.item:
|
||||
Voucher.clean_item_properties(
|
||||
{
|
||||
'block_quota': self.block_quota,
|
||||
},
|
||||
self.event,
|
||||
self.quota,
|
||||
self.item,
|
||||
self.variation
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_item_properties(data, event, quota, item, variation):
|
||||
if quota:
|
||||
if quota.event != event:
|
||||
raise ValidationError(_('You cannot select a quota that belongs to a different event.'))
|
||||
if item:
|
||||
raise ValidationError(_('You cannot select a quota and a specific product at the same time.'))
|
||||
elif self.item:
|
||||
if self.variation and (not self.item or not self.item.has_variations):
|
||||
elif item:
|
||||
if item.event != event:
|
||||
raise ValidationError(_('You cannot select an item that belongs to a different event.'))
|
||||
if variation and (not item or not item.has_variations):
|
||||
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
|
||||
'variations.'))
|
||||
if self.variation and not self.item.variations.filter(pk=self.variation.pk).exists():
|
||||
if variation and not item.variations.filter(pk=variation.pk).exists():
|
||||
raise ValidationError(_('This variation does not belong to this product.'))
|
||||
if self.item.has_variations and not self.variation and self.block_quota:
|
||||
if item.has_variations and not variation and data.get('block_quota'):
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
if item.category and item.category.is_addon:
|
||||
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
||||
else:
|
||||
raise ValidationError(_('You need to specify either a quota or a product.'))
|
||||
if self.event.has_subevents and self.block_quota and not self.subevent:
|
||||
|
||||
@staticmethod
|
||||
def clean_max_usages(data, redeemed):
|
||||
if data.get('max_usages', 1) < redeemed:
|
||||
raise ValidationError(
|
||||
_('This voucher has already been redeemed %(redeemed)s times. You cannot reduce the maximum number of '
|
||||
'usages below this number.'),
|
||||
params={
|
||||
'redeemed': redeemed
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_subevent(data, event):
|
||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||
elif data.get('subevent') and not event.has_subevents:
|
||||
raise ValidationError(_('You can not select a subevent if your event is not an event series.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_needs_checking(data, old_instance, item_changed, creating):
|
||||
# We only need to check for quota on vouchers that are now blocking quota and haven't
|
||||
# before (or have blocked a different quota before)
|
||||
if data.get('block_quota', False):
|
||||
is_valid = data.get('valid_until') is None or data.get('valid_until') >= now()
|
||||
if not is_valid:
|
||||
# If the voucher is not valid, it won't block any quota
|
||||
return False
|
||||
|
||||
if creating:
|
||||
# This is a new voucher
|
||||
return True
|
||||
|
||||
if not old_instance.block_quota:
|
||||
# Change from nonblocking to blocking
|
||||
return True
|
||||
|
||||
if old_instance.valid_until is not None and old_instance.valid_until < now():
|
||||
# This voucher has been expired and is now valid again and therefore blocks quota again
|
||||
return True
|
||||
|
||||
if item_changed:
|
||||
# The voucher has been reassigned to a different item, variation or quota
|
||||
return True
|
||||
|
||||
if data.get('subevent') != old_instance.subevent:
|
||||
# The voucher has been reassigned to a different subevent
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_get_ignored(old_instance):
|
||||
quotas = set()
|
||||
was_valid = old_instance and (
|
||||
old_instance.valid_until is None or old_instance.valid_until >= now()
|
||||
)
|
||||
if old_instance and old_instance.block_quota and was_valid:
|
||||
if old_instance.quota:
|
||||
quotas.add(old_instance.quota)
|
||||
elif old_instance.variation:
|
||||
quotas |= set(old_instance.variation.quotas.filter(
|
||||
subevent=old_instance.subevent))
|
||||
elif old_instance.item:
|
||||
quotas |= set(old_instance.item.quotas.filter(
|
||||
subevent=old_instance.subevent))
|
||||
return quotas
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
|
||||
|
||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||
|
||||
if quota:
|
||||
if quota in old_quotas:
|
||||
return
|
||||
else:
|
||||
avail = quota.availability(count_waitinglist=False)
|
||||
elif item and item.has_variations and not variation:
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
elif item and variation:
|
||||
avail = variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
elif item and not item.has_variations:
|
||||
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
else:
|
||||
raise ValidationError(_('You need to specify either a quota or a product.'))
|
||||
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
|
||||
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
|
||||
'quota is currently sold out or completely reserved.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_voucher_code(data, event, pk):
|
||||
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
|
||||
raise ValidationError(_('A voucher with this code already exists.'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.code = self.code.upper()
|
||||
super().save(*args, **kwargs)
|
||||
self.event.get_cache().set('vouchers_exist', True)
|
||||
self.event.cache.set('vouchers_exist', True)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
super().delete(using, keep_parents)
|
||||
self.event.get_cache().delete('vouchers_exist')
|
||||
self.event.cache.delete('vouchers_exist')
|
||||
|
||||
def is_in_cart(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -21,6 +21,7 @@ from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.presale.views import get_cart_total
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,6 +50,17 @@ class BasePaymentProvider:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
@property
|
||||
def is_meta(self) -> bool:
|
||||
"""
|
||||
Returns whether or whether not this payment provider is a "meta" payment provider that only
|
||||
works as a settings holder for other payment providers and should never be used directly. This
|
||||
is a trick to implement payment gateways with multiple payment methods but unified payment settings.
|
||||
Take a look at the built-in stripe provider to see how this might be used.
|
||||
By default, this returns ``False``.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""
|
||||
@@ -149,7 +161,9 @@ class BasePaymentProvider:
|
||||
('_fee_percent',
|
||||
forms.DecimalField(
|
||||
label=_('Additional fee'),
|
||||
help_text=_('Percentage'),
|
||||
help_text=_('Percentage of the order total. Note that this percentage will currently only '
|
||||
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
|
||||
'fees, if there are any.'),
|
||||
required=False
|
||||
)),
|
||||
('_availability_date',
|
||||
@@ -173,6 +187,7 @@ class BasePaymentProvider:
|
||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices.'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}}
|
||||
)),
|
||||
])
|
||||
|
||||
@@ -273,7 +288,7 @@ class BasePaymentProvider:
|
||||
|
||||
The default implementation checks for the _availability_date setting to be either unset or in the future.
|
||||
"""
|
||||
return self._is_still_available(cart_id=request.session.session_key)
|
||||
return self._is_still_available(cart_id=get_or_create_cart_id(request))
|
||||
|
||||
def payment_form_render(self, request: HttpRequest) -> str:
|
||||
"""
|
||||
@@ -591,7 +606,11 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
messages.success(request, _('The order has been marked as refunded.'))
|
||||
|
||||
def is_allowed(self, request: HttpRequest) -> bool:
|
||||
return get_cart_total(request) == 0
|
||||
from .services.cart import get_fees
|
||||
|
||||
total = get_cart_total(request)
|
||||
total += sum([f.value for f in get_fees(self.event, request, total, None, None)])
|
||||
return total == 0
|
||||
|
||||
def order_change_allowed(self, order: Order) -> bool:
|
||||
return False
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytz
|
||||
from dateutil import parser
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
BASE_CHOICES = (
|
||||
@@ -107,6 +108,9 @@ class RelativeDateWrapper:
|
||||
data = parser.parse(input)
|
||||
return RelativeDateWrapper(data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.to_string())
|
||||
|
||||
|
||||
class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
||||
@@ -168,6 +172,8 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
)
|
||||
if 'widget' not in kwargs:
|
||||
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
|
||||
kwargs.pop('max_length', 0)
|
||||
kwargs.pop('empty_value', 0)
|
||||
super().__init__(
|
||||
fields=fields, require_all_fields=False, *args, **kwargs
|
||||
)
|
||||
@@ -277,3 +283,34 @@ class RelativeDateField(RelativeDateTimeField):
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
|
||||
return super().clean(value)
|
||||
|
||||
|
||||
class ModelRelativeDateTimeField(models.CharField):
|
||||
form_class = RelativeDateTimeField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event', None)
|
||||
kwargs.setdefault('max_length', 255)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, RelativeDateWrapper):
|
||||
return value
|
||||
if value is None:
|
||||
return None
|
||||
return RelativeDateWrapper.from_string(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if isinstance(value, RelativeDateWrapper):
|
||||
return value.to_string()
|
||||
return value
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
if value is None:
|
||||
return None
|
||||
return RelativeDateWrapper.from_string(value)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': self.form_class}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import List, Optional
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
||||
|
||||
@@ -19,7 +20,11 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.services.async import ProfiledTask
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.celery_app import app
|
||||
from pretix.presale.signals import (
|
||||
checkout_confirm_messages, fee_calculation_for_cart,
|
||||
)
|
||||
|
||||
|
||||
class CartError(LazyLocaleException):
|
||||
@@ -56,6 +61,8 @@ error_messages = {
|
||||
'cart if you want to use it for a different product.'),
|
||||
'voucher_expired': _('This voucher is expired.'),
|
||||
'voucher_invalid_item': _('This voucher is not valid for this product.'),
|
||||
'voucher_item_not_available': _(
|
||||
'Your voucher is valid for a product that is currently not for sale.'),
|
||||
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
|
||||
'voucher_required': _('You need a valid voucher code to order this product.'),
|
||||
'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'),
|
||||
@@ -313,7 +320,7 @@ class CartManager:
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
|
||||
self._quota_diff += quota_diff
|
||||
self._quota_diff.update(quota_diff)
|
||||
self._voucher_use_diff += voucher_use_diff
|
||||
self._operations += operations
|
||||
|
||||
@@ -451,7 +458,7 @@ class CartManager:
|
||||
for k, v in al.items():
|
||||
if k not in input_addons[cp.id]:
|
||||
if v.expires > self.now_dt:
|
||||
quotas = list(cp.quotas)
|
||||
quotas = list(v.quotas)
|
||||
|
||||
for quota in quotas:
|
||||
quota_diff[quota] -= 1
|
||||
@@ -459,12 +466,14 @@ class CartManager:
|
||||
op = self.RemoveOperation(position=v)
|
||||
operations.append(op)
|
||||
|
||||
self._quota_diff += quota_diff
|
||||
self._quota_diff.update(quota_diff)
|
||||
self._operations += operations
|
||||
|
||||
def _get_quota_availability(self):
|
||||
quotas_ok = {}
|
||||
quotas_ok = defaultdict(int)
|
||||
for quota, count in self._quota_diff.items():
|
||||
if count <= 0:
|
||||
quotas_ok[quota] = 0
|
||||
avail = quota.availability(self.now_dt)
|
||||
if avail[1] is not None and avail[1] < count:
|
||||
quotas_ok[quota] = min(count, avail[1])
|
||||
@@ -536,6 +545,9 @@ class CartManager:
|
||||
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.RemoveOperation):
|
||||
if op.position.expires > self.now_dt:
|
||||
for q in op.position.quotas:
|
||||
quotas_ok[q] += 1
|
||||
op.position.delete()
|
||||
|
||||
elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
|
||||
@@ -627,13 +639,10 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
|
||||
return totaldiff
|
||||
|
||||
|
||||
def get_fees(event, total, invoice_address, provider):
|
||||
def get_fees(event, request, total, invoice_address, provider):
|
||||
fees = []
|
||||
|
||||
if total == 0:
|
||||
return fees
|
||||
|
||||
if provider:
|
||||
if provider and total != 0:
|
||||
provider = event.get_payment_providers().get(provider)
|
||||
if provider:
|
||||
payment_fee = provider.calculate_fee(total)
|
||||
@@ -643,7 +652,7 @@ def get_fees(event, total, invoice_address, provider):
|
||||
if payment_fee_tax_rule.tax_applicable(invoice_address):
|
||||
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
|
||||
fees.append(OrderFee(
|
||||
fee_type="PAYMENT",
|
||||
fee_type=OrderFee.FEE_TYPE_PAYMENT,
|
||||
value=payment_fee,
|
||||
tax_rate=payment_fee_tax.rate,
|
||||
tax_value=payment_fee_tax.tax,
|
||||
@@ -651,13 +660,16 @@ def get_fees(event, total, invoice_address, provider):
|
||||
))
|
||||
else:
|
||||
fees.append(OrderFee(
|
||||
fee_type="PAYMENT",
|
||||
fee_type=OrderFee.FEE_TYPE_PAYMENT,
|
||||
value=payment_fee,
|
||||
tax_rate=Decimal('0.00'),
|
||||
tax_value=Decimal('0.00'),
|
||||
tax_rule=payment_fee_tax_rule
|
||||
))
|
||||
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address):
|
||||
fees += resp
|
||||
|
||||
return fees
|
||||
|
||||
|
||||
@@ -760,3 +772,13 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@receiver(checkout_confirm_messages, dispatch_uid="cart_confirm_messages")
|
||||
def confirm_messages(sender, *args, **kwargs):
|
||||
if not sender.settings.confirm_text:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'confirm_text': rich_text(str(sender.settings.confirm_text))
|
||||
}
|
||||
|
||||
@@ -54,13 +54,14 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
{i.zipcode} {i.city}
|
||||
{country}""")
|
||||
invoice.invoice_to = addr_template.format(
|
||||
i=invoice.order.invoice_address,
|
||||
country=invoice.order.invoice_address.country.name if invoice.order.invoice_address.country else invoice.order.invoice_address.country_old
|
||||
i=ia,
|
||||
country=ia.country.name if ia.country else ia.country_old
|
||||
).strip()
|
||||
if invoice.order.invoice_address.vat_id:
|
||||
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % invoice.order.invoice_address.vat_id
|
||||
invoice.internal_reference = ia.internal_reference
|
||||
if ia.vat_id:
|
||||
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % ia.vat_id
|
||||
|
||||
cc = str(invoice.order.invoice_address.country)
|
||||
cc = str(ia.country)
|
||||
|
||||
if cc in EU_CURRENCIES and EU_CURRENCIES[cc] != invoice.event.currency:
|
||||
invoice.foreign_currency_display = EU_CURRENCIES[cc]
|
||||
|
||||
@@ -82,6 +82,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
'invoice_company': ''
|
||||
})
|
||||
body, body_md = render_mail(template, context)
|
||||
subject = str(subject).format_map(context)
|
||||
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
|
||||
|
||||
subject = str(subject)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from collections import Counter, namedtuple
|
||||
@@ -23,7 +24,11 @@ from pretix.base.models import (
|
||||
User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import CachedTicket, InvoiceAddress, OrderFee
|
||||
from pretix.base.models.orders import (
|
||||
CachedTicket, InvoiceAddress, OrderFee, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
@@ -34,7 +39,10 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.signals import order_paid, order_placed, periodic_task
|
||||
from pretix.base.signals import (
|
||||
allow_ticket_download, order_fee_calculation, order_paid, order_placed,
|
||||
periodic_task,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -45,7 +53,7 @@ error_messages = {
|
||||
'the quantity you selected. Please see below for details.'),
|
||||
'price_changed': _('The price of some of the items in your cart has changed in the '
|
||||
'meantime. Please see below for details.'),
|
||||
'internal': _("An internal error occured, please try again."),
|
||||
'internal': _("An internal error occurred, please try again."),
|
||||
'empty': _("Your cart is empty."),
|
||||
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s. We removed the "
|
||||
"surplus items from your cart."),
|
||||
@@ -72,7 +80,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
|
||||
force: bool=False, send_mail: bool=True, user: User=None, mail_text='') -> Order:
|
||||
force: bool=False, send_mail: bool=True, user: User=None, mail_text='',
|
||||
count_waitinglist=True, api_token=None) -> Order:
|
||||
"""
|
||||
Marks an order as paid. This sets the payment provider, info and date and returns
|
||||
the order object.
|
||||
@@ -98,7 +107,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
return order
|
||||
|
||||
with order.event.lock() as now_dt:
|
||||
can_be_paid = order._can_be_paid()
|
||||
can_be_paid = order._can_be_paid(count_waitinglist=count_waitinglist)
|
||||
if not force and can_be_paid is not True:
|
||||
raise Quota.QuotaExceededException(can_be_paid)
|
||||
order.payment_provider = provider or order.payment_provider
|
||||
@@ -115,7 +124,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
'date': date or now_dt,
|
||||
'manual': manual,
|
||||
'force': force
|
||||
}, user=user)
|
||||
}, user=user, api_token=api_token)
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
|
||||
@@ -154,6 +163,46 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
return order
|
||||
|
||||
|
||||
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, api_token=None):
|
||||
"""
|
||||
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
||||
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
|
||||
will be ignored.
|
||||
"""
|
||||
if new_date < now():
|
||||
raise OrderError(_('The new expiry date needs to be in the future.'))
|
||||
if order.status == Order.STATUS_PENDING:
|
||||
order.expires = new_date
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expirychanged',
|
||||
user=user,
|
||||
api_token=api_token,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
'state_change': False
|
||||
}
|
||||
)
|
||||
else:
|
||||
with order.event.lock() as now_dt:
|
||||
is_available = order._is_still_available(now_dt, count_waitinglist=False)
|
||||
if is_available is True or force is True:
|
||||
order.expires = new_date
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expirychanged',
|
||||
user=user,
|
||||
api_token=api_token,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
'state_change': True
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise OrderError(is_available)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def mark_order_refunded(order, user=None):
|
||||
"""
|
||||
@@ -178,7 +227,7 @@ def mark_order_refunded(order, user=None):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _cancel_order(order, user=None, send_mail: bool=True):
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
@@ -188,13 +237,15 @@ def _cancel_order(order, user=None, send_mail: bool=True):
|
||||
order = Order.objects.get(pk=order)
|
||||
if isinstance(user, int):
|
||||
user = User.objects.get(pk=user)
|
||||
if isinstance(api_token, int):
|
||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||
with order.event.lock():
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
raise OrderError(_('You cannot cancel this order.'))
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
|
||||
order.log_action('pretix.event.order.canceled', user=user)
|
||||
order.log_action('pretix.event.order.canceled', user=user, api_token=api_token)
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
@@ -342,7 +393,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
raise OrderError(err, errargs)
|
||||
|
||||
|
||||
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider):
|
||||
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider, address: InvoiceAddress,
|
||||
meta_info: dict, event: Event):
|
||||
fees = []
|
||||
total = sum([c.price for c in positions])
|
||||
payment_fee = payment_provider.calculate_fee(total)
|
||||
@@ -350,15 +402,18 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
||||
fees.append(OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
|
||||
internal_type=payment_provider.identifier))
|
||||
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address,
|
||||
meta_info=meta_info, posiitons=positions):
|
||||
fees += resp
|
||||
return fees
|
||||
|
||||
|
||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
|
||||
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
|
||||
meta_info: dict=None):
|
||||
from datetime import time
|
||||
|
||||
fees = _get_fees(positions, payment_provider)
|
||||
fees = _get_fees(positions, payment_provider, address, meta_info, event)
|
||||
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
@@ -537,7 +592,11 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
|
||||
if eventsettings.payment_term_expire_automatically:
|
||||
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
|
||||
else:
|
||||
email_subject = _('Your order is pending payment: %(code)s') % {'code': o.code}
|
||||
|
||||
try:
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
@@ -561,6 +620,9 @@ def send_download_reminders(sender, **kwargs):
|
||||
if now() < reminder_date:
|
||||
continue
|
||||
for o in e.orders.filter(status=Order.STATUS_PAID, download_reminder_sent=False):
|
||||
if not all([r for rr, r in allow_ticket_download.send(e, order=o)]):
|
||||
continue
|
||||
|
||||
o.download_reminder_sent = True
|
||||
o.save()
|
||||
email_template = e.settings.mail_text_download_reminder
|
||||
@@ -603,13 +665,16 @@ class OrderChangeManager:
|
||||
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position',))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
|
||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||
|
||||
def __init__(self, order: Order, user):
|
||||
def __init__(self, order: Order, user, notify=True):
|
||||
self.order = order
|
||||
self.user = user
|
||||
self.split_order = None
|
||||
self._totaldiff = 0
|
||||
self._quotadiff = Counter()
|
||||
self._operations = []
|
||||
self.notify = notify
|
||||
self._invoice_dirty = False
|
||||
|
||||
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
|
||||
@@ -727,6 +792,12 @@ class OrderChangeManager:
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent))
|
||||
|
||||
def split(self, position: OrderPosition):
|
||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._operations.append(self.SplitOperation(position))
|
||||
|
||||
def _check_quotas(self):
|
||||
for quota, diff in self._quotadiff.items():
|
||||
if diff <= 0:
|
||||
@@ -746,12 +817,26 @@ class OrderChangeManager:
|
||||
def _check_paid_to_free(self):
|
||||
if self.order.total == 0:
|
||||
try:
|
||||
mark_order_paid(self.order, 'free', send_mail=False)
|
||||
mark_order_paid(
|
||||
self.order, 'free', send_mail=False, count_waitinglist=False,
|
||||
user=self.user
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(self.error_messages['paid_to_free_exceeded'])
|
||||
|
||||
if self.split_order and self.split_order.total == 0:
|
||||
try:
|
||||
mark_order_paid(
|
||||
self.split_order, 'free', send_mail=False, count_waitinglist=False,
|
||||
user=self.user
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
raise OrderError(self.error_messages['paid_to_free_exceeded'])
|
||||
|
||||
def _perform_operations(self):
|
||||
nextposid = self.order.positions.aggregate(m=Max('positionid'))['m'] + 1
|
||||
split_positions = []
|
||||
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.ItemOperation):
|
||||
self.order.log_action('pretix.event.order.changed.item', user=self.user, data={
|
||||
@@ -836,23 +921,89 @@ class OrderChangeManager:
|
||||
'positionid': pos.positionid,
|
||||
'subevent': op.subevent.pk if op.subevent else None,
|
||||
})
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
split_positions.append(op.position)
|
||||
|
||||
def _recalculate_total_and_payment_fee(self):
|
||||
payment_fee = Decimal('0.00')
|
||||
if self.order.total != 0:
|
||||
prov = self._get_payment_provider()
|
||||
if prov:
|
||||
payment_fee = prov.calculate_fee(self.order.total)
|
||||
if split_positions:
|
||||
self.split_order = self._create_split_order(split_positions)
|
||||
|
||||
if payment_fee:
|
||||
fee = self.order.fees.get_or_create(fee_type=OrderFee.FEE_TYPE_PAYMENT, defaults={'value': 0})[0]
|
||||
def _create_split_order(self, split_positions):
|
||||
split_order = Order.objects.get(pk=self.order.pk)
|
||||
split_order.pk = None
|
||||
split_order.code = None
|
||||
split_order.datetime = now()
|
||||
split_order.secret = generate_secret()
|
||||
split_order.save()
|
||||
split_order.log_action('pretix.event.order.changed.split_from', user=self.user, data={
|
||||
'original_order': self.order.code
|
||||
})
|
||||
|
||||
for op in split_positions:
|
||||
self.order.log_action('pretix.event.order.changed.split', user=self.user, data={
|
||||
'position': op.pk,
|
||||
'positionid': op.positionid,
|
||||
'old_item': op.item.pk,
|
||||
'old_variation': op.variation.pk if op.variation else None,
|
||||
'old_price': op.price,
|
||||
'new_order': split_order.code,
|
||||
})
|
||||
op.order = split_order
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
|
||||
try:
|
||||
ia = copy.copy(self.order.invoice_address)
|
||||
ia.pk = None
|
||||
ia.order = split_order
|
||||
ia.save()
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
split_order.total = sum([p.price for p in split_positions])
|
||||
if split_order.total != Decimal('0.00') and self.order.status != Order.STATUS_PAID:
|
||||
payment_fee = self._get_payment_provider().calculate_fee(split_order.total)
|
||||
fee = split_order.fees.get_or_create(fee_type=OrderFee.FEE_TYPE_PAYMENT, defaults={'value': 0})[0]
|
||||
fee.value = payment_fee
|
||||
fee._calculate_tax()
|
||||
fee.save()
|
||||
else:
|
||||
self.order.fees.filter(fee_type=OrderFee.FEE_TYPE_PAYMENT).delete()
|
||||
if payment_fee != 0:
|
||||
fee.save()
|
||||
elif fee.pk:
|
||||
fee.delete()
|
||||
split_order.total += fee.value
|
||||
|
||||
self.order.total = sum([p.price for p in self.order.positions.all()]) + sum([f.value for f in self.order.fees.all()])
|
||||
for fee in self.order.fees.exclude(fee_type=OrderFee.FEE_TYPE_PAYMENT):
|
||||
new_fee = copy.copy(fee)
|
||||
new_fee.pk = None
|
||||
new_fee.order = split_order
|
||||
split_order.total += new_fee.value
|
||||
new_fee.save()
|
||||
|
||||
split_order.save()
|
||||
|
||||
if split_order.total != Decimal('0.00') and self.order.invoices.filter(is_cancellation=False).last():
|
||||
generate_invoice(split_order)
|
||||
return split_order
|
||||
|
||||
def _recalculate_total_and_payment_fee(self):
|
||||
self.order.total = sum([p.price for p in self.order.positions.all()])
|
||||
|
||||
if self.order.status != Order.STATUS_PAID:
|
||||
# Do not change payment fees of paid orders
|
||||
payment_fee = Decimal('0.00')
|
||||
if self.order.total != 0:
|
||||
prov = self._get_payment_provider()
|
||||
if prov:
|
||||
payment_fee = prov.calculate_fee(self.order.total)
|
||||
|
||||
if payment_fee:
|
||||
fee = self.order.fees.get_or_create(fee_type=OrderFee.FEE_TYPE_PAYMENT, defaults={'value': 0})[0]
|
||||
fee.value = payment_fee
|
||||
fee._calculate_tax()
|
||||
fee.save()
|
||||
else:
|
||||
self.order.fees.filter(fee_type=OrderFee.FEE_TYPE_PAYMENT).delete()
|
||||
|
||||
self.order.total += sum([f.value for f in self.order.fees.all()])
|
||||
self.order.save()
|
||||
|
||||
def _reissue_invoice(self):
|
||||
@@ -862,8 +1013,9 @@ class OrderChangeManager:
|
||||
generate_invoice(self.order)
|
||||
|
||||
def _check_complete_cancel(self):
|
||||
cancels = len([o for o in self._operations if isinstance(o, self.CancelOperation)])
|
||||
if cancels == self.order.positions.count():
|
||||
cancels = len([o for o in self._operations if isinstance(o, (self.CancelOperation, self.SplitOperation))])
|
||||
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
|
||||
if self.order.positions.count() - cancels + adds < 1:
|
||||
raise OrderError(self.error_messages['complete_cancel'])
|
||||
|
||||
@property
|
||||
@@ -873,27 +1025,27 @@ class OrderChangeManager:
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
return None
|
||||
|
||||
def _notify_user(self):
|
||||
with language(self.order.locale):
|
||||
def _notify_user(self, order):
|
||||
with language(order.locale):
|
||||
try:
|
||||
invoice_name = self.order.invoice_address.name
|
||||
invoice_company = self.order.invoice_address.company
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_template = self.order.event.settings.mail_text_order_changed
|
||||
email_template = order.event.settings.mail_text_order_changed
|
||||
email_context = {
|
||||
'event': self.order.event.name,
|
||||
'event': order.event.name,
|
||||
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order has been changed: %(code)s') % {'code': self.order.code}
|
||||
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
self.order.send_mail(
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_changed', self.user
|
||||
)
|
||||
@@ -917,7 +1069,10 @@ class OrderChangeManager:
|
||||
self._reissue_invoice()
|
||||
self._clear_tickets_cache()
|
||||
self._check_paid_to_free()
|
||||
self._notify_user()
|
||||
if self.notify:
|
||||
self._notify_user(self.order)
|
||||
if self.split_order:
|
||||
self._notify_user(self.split_order)
|
||||
|
||||
def _clear_tickets_cache(self):
|
||||
CachedTicket.objects.filter(order_position__order=self.order).delete()
|
||||
@@ -943,10 +1098,10 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True):
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None):
|
||||
try:
|
||||
try:
|
||||
return _cancel_order(order, user, send_mail)
|
||||
return _cancel_order(order, user, send_mail, api_token)
|
||||
except LockTimeoutException:
|
||||
self.retry(exc=OrderError(error_messages['busy']))
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
|
||||
32
src/pretix/base/services/quotas.py
Normal file
32
src/pretix/base/services/quotas.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.db import models
|
||||
from django.db.models import F, Max, OuterRef, Q, Subquery
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.models import LogEntry, Quota
|
||||
from pretix.celery_app import app
|
||||
|
||||
from ..signals import periodic_task
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
def build_all_quota_caches(sender, **kwargs):
|
||||
refresh_quota_caches.apply_async()
|
||||
|
||||
|
||||
@app.task
|
||||
def refresh_quota_caches():
|
||||
last_activity = LogEntry.objects.filter(
|
||||
event=OuterRef('event_id'),
|
||||
).order_by().values('event').annotate(
|
||||
m=Max('datetime')
|
||||
).values(
|
||||
'm'
|
||||
)
|
||||
quotas = Quota.objects.annotate(
|
||||
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
|
||||
).filter(
|
||||
Q(cached_availability_time__isnull=True) |
|
||||
Q(cached_availability_time__lt=F('last_activity'))
|
||||
)
|
||||
for q in quotas:
|
||||
q.availability()
|
||||
@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.signals import order_fee_type_name
|
||||
|
||||
|
||||
class DummyObject:
|
||||
@@ -199,9 +200,15 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
|
||||
for pprov, total in sorted(num_total.items(), key=lambda i: i[0]):
|
||||
ppobj = DummyObject()
|
||||
if pprov[0] == OrderFee.FEE_TYPE_PAYMENT:
|
||||
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], provider_names.get(pprov[1], pprov[1]))
|
||||
ppobj.name = '{} - {}'.format(names[pprov[0]], provider_names.get(pprov[1], pprov[1]))
|
||||
else:
|
||||
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], pprov[1])
|
||||
name = pprov[1]
|
||||
for r, resp in order_fee_type_name.send(sender=event, fee_type=pprov[0], internal_type=pprov[1]):
|
||||
if resp:
|
||||
name = resp
|
||||
break
|
||||
|
||||
ppobj.name = '{} - {}'.format(names[pprov[0]], name)
|
||||
ppobj.provider = pprov[1]
|
||||
ppobj.has_variations = False
|
||||
ppobj.num_total = total
|
||||
|
||||
@@ -28,33 +28,34 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
|
||||
|
||||
sent = 0
|
||||
|
||||
for wle in qs:
|
||||
if (wle.item, wle.variation) in gone:
|
||||
continue
|
||||
|
||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.quotas.filter(subevent=wle.subevent))
|
||||
availability = (
|
||||
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
||||
)
|
||||
if availability[1] > 0:
|
||||
try:
|
||||
wle.send_voucher(quota_cache, user=user)
|
||||
sent += 1
|
||||
except WaitingListException: # noqa
|
||||
with event.lock():
|
||||
for wle in qs:
|
||||
if (wle.item, wle.variation) in gone:
|
||||
continue
|
||||
|
||||
# Reduce affected quotas in cache
|
||||
for q in quotas:
|
||||
quota_cache[q.pk] = (
|
||||
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
|
||||
quota_cache[q.pk][1] - 1
|
||||
)
|
||||
else:
|
||||
gone.add((wle.item, wle.variation))
|
||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.quotas.filter(subevent=wle.subevent))
|
||||
availability = (
|
||||
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
||||
)
|
||||
if availability[1] > 0:
|
||||
try:
|
||||
wle.send_voucher(quota_cache, user=user)
|
||||
sent += 1
|
||||
except WaitingListException: # noqa
|
||||
continue
|
||||
|
||||
# Reduce affected quotas in cache
|
||||
for q in quotas:
|
||||
quota_cache[q.pk] = (
|
||||
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
|
||||
quota_cache[q.pk][1] - 1
|
||||
)
|
||||
else:
|
||||
gone.add((wle.item, wle.variation))
|
||||
|
||||
return sent
|
||||
|
||||
|
||||
@@ -209,6 +209,10 @@ DEFAULTS = {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'confirm_text': {
|
||||
'default': None,
|
||||
'type': LazyI18nString
|
||||
},
|
||||
'mail_prefix': {
|
||||
'default': None,
|
||||
'type': str
|
||||
@@ -423,6 +427,14 @@ Your {event} team"""))
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'presale_widget_css_file': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'presale_widget_css_checksum': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'logo_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
|
||||
@@ -138,7 +138,9 @@ order_placed = EventPluginSignal(
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is placed. The order object is given
|
||||
as the first argument.
|
||||
as the first argument. This signal is *not* sent out if an order is created through
|
||||
splitting an existing order, so you can not expect to see all orders by listening
|
||||
to this signal.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -148,7 +150,8 @@ order_paid = EventPluginSignal(
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time an order is paid. The order object is given
|
||||
as the first argument.
|
||||
as the first argument. This signal is *not* sent out if an order is marked as paid
|
||||
because it an already-paid order has been splitted.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -166,6 +169,34 @@ to the user. The receivers are expected to return plain text.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
logentry_object_link = EventPluginSignal(
|
||||
providing_args=["logentry"]
|
||||
)
|
||||
"""
|
||||
To display the relationship of an instance of the ``LogEntry`` model to another model
|
||||
to a human user, ``pretix.base.signals.logentry_object_link`` will be sent out with a
|
||||
``logentry`` argument.
|
||||
|
||||
The first received response that is not ``None`` will be used to display the related object
|
||||
to the user. The receivers are expected to return a HTML link. The internal implementation
|
||||
builds the links like this::
|
||||
|
||||
a_text = _('Tax rule {val}')
|
||||
a_map = {
|
||||
'href': reverse('control:event.settings.tax.edit', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug,
|
||||
'rule': logentry.content_object.id
|
||||
}),
|
||||
'val': escape(logentry.content_object.name),
|
||||
}
|
||||
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
|
||||
return a_text.format_map(a_map)
|
||||
|
||||
Make sure that any user content in the HTML code you return is properly escaped!
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
requiredaction_display = EventPluginSignal(
|
||||
providing_args=["action", "request"]
|
||||
)
|
||||
@@ -209,3 +240,37 @@ register_global_settings = django.dispatch.Signal()
|
||||
All plugins that are installed may send fields for the global settings form, as
|
||||
an OrderedDict of (setting name, form field).
|
||||
"""
|
||||
|
||||
order_fee_calculation = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
)
|
||||
"""
|
||||
This signals allows you to add fees to an order while it is being created. You are expected to
|
||||
return a list of ``OrderFee`` objects that are not yet saved to the database
|
||||
(because there is no order yet).
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``positions``
|
||||
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary.
|
||||
"""
|
||||
|
||||
order_fee_type_name = EventPluginSignal(
|
||||
providing_args=['request', 'fee']
|
||||
)
|
||||
"""
|
||||
This signals allows you to return a human-readable description for a fee type based on the ``fee_type``
|
||||
and ``internal_type`` attributes of the ``OrderFee`` model that you get as keyword arguments. You are
|
||||
expected to return a string or None, if you don't know about this fee.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
allow_ticket_download = EventPluginSignal(
|
||||
providing_args=['order']
|
||||
)
|
||||
"""
|
||||
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
|
||||
a download will not be offered.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ import bleach
|
||||
import markdown
|
||||
from bleach import DEFAULT_CALLBACKS
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
@@ -56,13 +57,19 @@ ALLOWED_ATTRIBUTES = {
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not is_safe_url(url):
|
||||
if not is_safe_url(url) and not url.startswith('mailto:'):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
return attrs
|
||||
|
||||
|
||||
def abslink_callback(attrs, new=False):
|
||||
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, attrs.get((None, 'href'), '/'))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
return attrs
|
||||
|
||||
|
||||
@register.filter
|
||||
def rich_text(text: str, **kwargs):
|
||||
"""
|
||||
@@ -73,5 +80,5 @@ def rich_text(text: str, **kwargs):
|
||||
markdown.markdown(text),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
|
||||
), callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]))
|
||||
return mark_safe(body_md)
|
||||
|
||||
@@ -4,7 +4,17 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def url_replace(request, field, value):
|
||||
def url_replace(request, *pairs):
|
||||
dict_ = request.GET.copy()
|
||||
dict_[field] = value
|
||||
return dict_.urlencode()
|
||||
key = None
|
||||
for p in pairs:
|
||||
if key is None:
|
||||
key = p
|
||||
else:
|
||||
if p == "":
|
||||
if key in dict_:
|
||||
del dict_[key]
|
||||
else:
|
||||
dict_[key] = p
|
||||
key = None
|
||||
return dict_.urlencode(safe='[]')
|
||||
|
||||
@@ -32,6 +32,8 @@ class EventSlugBlacklistValidator(BlacklistValidator):
|
||||
'__debug__',
|
||||
'api',
|
||||
'events',
|
||||
'csp_report',
|
||||
'widget',
|
||||
]
|
||||
|
||||
|
||||
@@ -51,4 +53,6 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
|
||||
'__debug__',
|
||||
'about',
|
||||
'api',
|
||||
'csp_report',
|
||||
'widget',
|
||||
]
|
||||
|
||||
@@ -51,6 +51,9 @@ class AsyncAction:
|
||||
return self.get_result(request)
|
||||
return self.http_method_not_allowed(request)
|
||||
|
||||
def _ajax_response_data(self):
|
||||
return {}
|
||||
|
||||
def _return_ajax_result(self, res, timeout=.5):
|
||||
if not res.ready():
|
||||
try:
|
||||
@@ -59,10 +62,11 @@ class AsyncAction:
|
||||
pass
|
||||
|
||||
ready = res.ready()
|
||||
data = {
|
||||
data = self._ajax_response_data()
|
||||
data.update({
|
||||
'async_id': res.id,
|
||||
'ready': ready
|
||||
}
|
||||
})
|
||||
if ready:
|
||||
if res.successful() and not isinstance(res.info, Exception):
|
||||
smes = self.get_success_message(res.info)
|
||||
@@ -129,7 +133,7 @@ class AsyncAction:
|
||||
return str(exception)
|
||||
else:
|
||||
logger.error('Unexpected exception: %r' % exception)
|
||||
return _('An unexpected error has occured.')
|
||||
return _('An unexpected error has occurred.')
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _('The task has been completed.')
|
||||
|
||||
24
src/pretix/base/views/csp.py
Normal file
24
src/pretix/base/views/csp.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
logger = logging.getLogger('pretix.security.csp')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def csp_report(request):
|
||||
try:
|
||||
body = json.loads(request.body.decode())
|
||||
logger.warning(
|
||||
'CSP violation at {r[document-uri]}\n'
|
||||
'Referer: {r[referrer]}\n'
|
||||
'Blocked: {r[blocked-uri]}\n'
|
||||
'Violated: {r[violated-directive]}\n'
|
||||
'Original polity: {r[original-policy]}'.format(r=body['csp-report'])
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
logger.exception('CSP report failed ' + str(e))
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponse()
|
||||
@@ -29,14 +29,14 @@ def contextprocessor(request):
|
||||
'DEBUG': settings.DEBUG,
|
||||
}
|
||||
_html_head = []
|
||||
if hasattr(request, 'event'):
|
||||
if hasattr(request, 'event') and request.user.is_authenticated:
|
||||
for receiver, response in html_head.send(request.event, request=request):
|
||||
_html_head.append(response)
|
||||
ctx['html_head'] = "".join(_html_head)
|
||||
|
||||
_js_payment_weekdays_disabled = '[]'
|
||||
_nav_event = []
|
||||
if getattr(request, 'event', None) and hasattr(request, 'organizer'):
|
||||
if getattr(request, 'event', None) and hasattr(request, 'organizer') and request.user.is_authenticated:
|
||||
for receiver, response in nav_event.send(request.event, request=request):
|
||||
_nav_event += response
|
||||
if request.event.settings.get('payment_term_weekdays'):
|
||||
@@ -61,15 +61,16 @@ def contextprocessor(request):
|
||||
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
|
||||
|
||||
_nav_global = []
|
||||
if not hasattr(request, 'event'):
|
||||
if not hasattr(request, 'event') and request.user.is_authenticated:
|
||||
for receiver, response in nav_global.send(request, request=request):
|
||||
_nav_global += response
|
||||
|
||||
ctx['nav_global'] = sorted(_nav_global, key=lambda n: n['label'])
|
||||
|
||||
_nav_topbar = []
|
||||
for receiver, response in nav_topbar.send(request, request=request):
|
||||
_nav_topbar += response
|
||||
if request.user.is_authenticated:
|
||||
for receiver, response in nav_topbar.send(request, request=request):
|
||||
_nav_topbar += response
|
||||
ctx['nav_topbar'] = sorted(_nav_topbar, key=lambda n: n['label'])
|
||||
|
||||
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ...base.forms import I18nModelForm
|
||||
@@ -98,3 +100,34 @@ class SlugWidget(forms.TextInput):
|
||||
ctx = super().get_context(name, value, attrs)
|
||||
ctx['pre'] = self.prefix
|
||||
return ctx
|
||||
|
||||
|
||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
date_attrs = dict(attrs)
|
||||
time_attrs = dict(attrs)
|
||||
date_attrs.setdefault('class', 'form-control splitdatetimepart')
|
||||
time_attrs.setdefault('class', 'form-control splitdatetimepart')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
date_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
time_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
widgets = (
|
||||
forms.DateInput(attrs=date_attrs, format=date_format),
|
||||
forms.TimeInput(attrs=time_attrs, format=time_format),
|
||||
)
|
||||
# Skip one hierarchy level
|
||||
forms.MultiWidget.__init__(self, widgets, attrs)
|
||||
|
||||
@@ -4,15 +4,17 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from pytz import common_timezones, timezone
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||
from pretix.base.models import Event, Organizer, TaxRule
|
||||
from pretix.base.models.event import EventMetaValue
|
||||
from pretix.base.models.event import EventMetaValue, SubEvent
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.control.forms import ExtFileField, SlugWidget
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -46,6 +48,8 @@ class EventWizardFoundationForm(forms.Form):
|
||||
empty_label=None,
|
||||
required=True
|
||||
)
|
||||
if len(self.fields['organizer'].choices) == 1:
|
||||
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
|
||||
|
||||
|
||||
class EventWizardBasicsForm(I18nModelForm):
|
||||
@@ -54,7 +58,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
}
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
label=_("Default timezone"),
|
||||
label=_("Event timezone"),
|
||||
)
|
||||
locale = forms.ChoiceField(
|
||||
choices=settings.LANGUAGES,
|
||||
@@ -64,7 +68,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
label=_("Sales tax rate"),
|
||||
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
|
||||
"here in percent. If you have a more complicated tax situation, you can add more tax rates and "
|
||||
"detailled configuration later."),
|
||||
"detailed configuration later."),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -80,14 +84,18 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
'presale_end',
|
||||
'location',
|
||||
]
|
||||
field_classes = {
|
||||
'date_from': forms.SplitDateTimeField,
|
||||
'date_to': forms.SplitDateTimeField,
|
||||
'presale_start': forms.SplitDateTimeField,
|
||||
'presale_end': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_basics-date_from'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_basics-presale_start'}),
|
||||
'slug': SlugWidget
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
|
||||
'slug': SlugWidget,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -99,6 +107,9 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
self.initial['timezone'] = get_current_timezone_name()
|
||||
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
'Sample Conference Center\nHeidelberg, Germany'
|
||||
)
|
||||
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
|
||||
if self.has_subevents:
|
||||
del self.fields['presale_start']
|
||||
@@ -188,6 +199,9 @@ class EventUpdateForm(I18nModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
'Sample Conference Center\nHeidelberg, Germany'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -204,14 +218,19 @@ class EventUpdateForm(I18nModelForm):
|
||||
'presale_end',
|
||||
'location',
|
||||
]
|
||||
field_classes = {
|
||||
'date_from': forms.SplitDateTimeField,
|
||||
'date_to': forms.SplitDateTimeField,
|
||||
'date_admission': forms.SplitDateTimeField,
|
||||
'presale_start': forms.SplitDateTimeField,
|
||||
'presale_end': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
|
||||
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-default': '#id_date_from'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_presale_start'}),
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +265,7 @@ class EventSettingsForm(SettingsForm):
|
||||
last_order_modification_date = RelativeDateTimeField(
|
||||
label=_('Last date of modifications'),
|
||||
help_text=_("The last date users can modify details of their orders, such as attendee names or "
|
||||
"answers to questions. If you use the event series feature and an order contains tickest for "
|
||||
"answers to questions. If you use the event series feature and an order contains tickets for "
|
||||
"multiple event dates, the earliest date will be used."),
|
||||
required=False,
|
||||
)
|
||||
@@ -339,6 +358,14 @@ class EventSettingsForm(SettingsForm):
|
||||
label=_("Imprint URL"),
|
||||
required=False,
|
||||
)
|
||||
confirm_text = I18nFormField(
|
||||
label=_('Confirmation text'),
|
||||
help_text=_('This text needs to be confirmed by the user before a purchase is possible. You could for example '
|
||||
'link your terms of service here. If you use the Pages feature to publish your terms of service, '
|
||||
'you don\'t need this setting since you can configure it there.'),
|
||||
required=False,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
contact_mail = forms.EmailField(
|
||||
label=_("Contact address"),
|
||||
required=False,
|
||||
@@ -366,6 +393,14 @@ class EventSettingsForm(SettingsForm):
|
||||
})
|
||||
return data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['confirm_text'].widget.attrs['rows'] = '3'
|
||||
self.fields['confirm_text'].widget.attrs['placeholder'] = _(
|
||||
'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
|
||||
'and agree with them.'
|
||||
)
|
||||
|
||||
|
||||
class PaymentSettingsForm(SettingsForm):
|
||||
payment_term_days = forms.IntegerField(
|
||||
@@ -517,25 +552,51 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
choices=[]
|
||||
)
|
||||
invoice_address_from = forms.CharField(
|
||||
widget=forms.Textarea(attrs={'rows': 5}), required=False,
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 5,
|
||||
'placeholder': _(
|
||||
'Sample Event Company\n'
|
||||
'Albert Einstein Road 52\n'
|
||||
'12345 Samplecity'
|
||||
)
|
||||
}),
|
||||
required=False,
|
||||
label=_("Your address"),
|
||||
help_text=_("Will be printed as the sender on invoices. Be sure to include relevant details required in "
|
||||
"your jurisdiction (e.g. your VAT ID).")
|
||||
"your jurisdiction.")
|
||||
)
|
||||
invoice_introductory_text = I18nFormField(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': 3,
|
||||
'placeholder': _(
|
||||
'e.g. With this document, we sent you the invoice for your ticket order.'
|
||||
)
|
||||
}},
|
||||
required=False,
|
||||
label=_("Introductory text"),
|
||||
help_text=_("Will be printed on every invoice above the invoice rows.")
|
||||
)
|
||||
invoice_additional_text = I18nFormField(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': 3,
|
||||
'placeholder': _(
|
||||
'e.g. Thank you for your purchase! You can find more information on the event at ...'
|
||||
)
|
||||
}},
|
||||
required=False,
|
||||
label=_("Additional text"),
|
||||
help_text=_("Will be printed on every invoice below the invoice total.")
|
||||
)
|
||||
invoice_footer_text = I18nFormField(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': 5,
|
||||
'placeholder': _(
|
||||
'e.g. your bank details, legal details like your VAT ID, registration numbers, etc.'
|
||||
)
|
||||
}},
|
||||
required=False,
|
||||
label=_("Footer"),
|
||||
help_text=_("Will be printed centered and in a smaller font at the end of every invoice page.")
|
||||
@@ -578,7 +639,13 @@ class MailSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will be attached to every email. Available placeholders: {event}"),
|
||||
validators=[PlaceholderValidator(['{event}'])]
|
||||
validators=[PlaceholderValidator(['{event}'])],
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': '4',
|
||||
'placeholder': _(
|
||||
'e.g. your contact details'
|
||||
)
|
||||
}}
|
||||
)
|
||||
|
||||
mail_text_order_placed = I18nFormField(
|
||||
@@ -683,14 +750,17 @@ class MailSettingsForm(SettingsForm):
|
||||
)
|
||||
smtp_host = forms.CharField(
|
||||
label=_("Hostname"),
|
||||
required=False
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'})
|
||||
)
|
||||
smtp_port = forms.IntegerField(
|
||||
label=_("Port"),
|
||||
required=False
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'})
|
||||
)
|
||||
smtp_username = forms.CharField(
|
||||
label=_("Username"),
|
||||
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
|
||||
required=False
|
||||
)
|
||||
smtp_password = forms.CharField(
|
||||
@@ -827,3 +897,44 @@ class TaxRuleForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country']
|
||||
|
||||
|
||||
class WidgetCodeForm(forms.Form):
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', "Date"),
|
||||
required=True,
|
||||
queryset=SubEvent.objects.none()
|
||||
)
|
||||
language = forms.ChoiceField(
|
||||
label=_("Language"),
|
||||
required=True,
|
||||
choices=settings.LANGUAGES
|
||||
)
|
||||
voucher = forms.CharField(
|
||||
label=_("Pre-selected voucher"),
|
||||
required=False,
|
||||
help_text=_("If set, the widget will show products as if this voucher has been entered and when a product is "
|
||||
"bought via the widget, this voucher will be used. This can for example be used to provide "
|
||||
"widgets that give discounts or unlock secret products.")
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
self.fields['language'].choices = [(l, n) for l, n in settings.LANGUAGES if l in self.event.settings.locales]
|
||||
|
||||
def clean_voucher(self):
|
||||
v = self.cleaned_data.get('voucher')
|
||||
if not v:
|
||||
return
|
||||
|
||||
if not self.event.vouchers.filter(code=v).exists():
|
||||
raise ValidationError(_('The given voucher code does not exist.'))
|
||||
|
||||
return v
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Invoice, Item, Order, Organizer, SubEvent
|
||||
from pretix.base.models import Event, Invoice, Item, Order, Organizer, SubEvent
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.utils.i18n import i18ncomp
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
|
||||
PAYMENT_PROVIDERS = []
|
||||
|
||||
|
||||
def get_all_payment_providers():
|
||||
global PAYMENT_PROVIDERS
|
||||
|
||||
if PAYMENT_PROVIDERS:
|
||||
return PAYMENT_PROVIDERS
|
||||
|
||||
with rolledback_transaction():
|
||||
event = Event.objects.create(
|
||||
plugins=",".join([app.name for app in apps.get_app_configs()]),
|
||||
name="INTERNAL",
|
||||
date_from=now(),
|
||||
organizer=Organizer.objects.create(name="INTERNAL")
|
||||
)
|
||||
provs = register_payment_providers.send(
|
||||
sender=event
|
||||
)
|
||||
choices = []
|
||||
for recv, prov in provs:
|
||||
if isinstance(prov, list):
|
||||
for p in prov:
|
||||
p = p(event)
|
||||
if not p.is_meta:
|
||||
choices.append((p.identifier, p.verbose_name))
|
||||
else:
|
||||
prov = prov(event)
|
||||
if not prov.is_meta:
|
||||
choices.append((prov.identifier, prov.verbose_name))
|
||||
PAYMENT_PROVIDERS = choices
|
||||
return choices
|
||||
|
||||
|
||||
class FilterForm(forms.Form):
|
||||
@@ -36,6 +71,13 @@ class OrderFilterForm(FilterForm):
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
provider = forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
choices=[
|
||||
('', _('All payment providers')),
|
||||
],
|
||||
required=False,
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Order status'),
|
||||
choices=(
|
||||
@@ -93,23 +135,25 @@ class OrderFilterForm(FilterForm):
|
||||
else:
|
||||
qs = qs.filter(status=s)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(dict(self.fields['ordering'].choices)[fdata.get('ordering')])
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class EventOrderFilterForm(OrderFilterForm):
|
||||
orders = {'code': 'code', 'email': 'email', 'total': 'total',
|
||||
'datetime': 'datetime', 'status': 'status', 'pcnt': 'pcnt'}
|
||||
|
||||
item = forms.ModelChoiceField(
|
||||
label=_('Products'),
|
||||
queryset=Item.objects.none(),
|
||||
required=False,
|
||||
empty_label=_('All products')
|
||||
)
|
||||
provider = forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
choices=[
|
||||
('', _('All payment providers')),
|
||||
],
|
||||
required=False,
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
queryset=SubEvent.objects.none(),
|
||||
@@ -117,17 +161,6 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
|
||||
def get_payment_providers(self):
|
||||
providers = []
|
||||
responses = register_payment_providers.send(self.event)
|
||||
for receiver, response in responses:
|
||||
provider = response(self.event)
|
||||
providers.append({
|
||||
'name': provider.identifier,
|
||||
'verbose_name': provider.verbose_name
|
||||
})
|
||||
return providers
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -150,13 +183,14 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
if fdata.get('subevent'):
|
||||
qs = qs.filter(positions__subevent=fdata.get('subevent'))
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class OrderSearchFilterForm(OrderFilterForm):
|
||||
orders = {'code': 'code', 'email': 'email', 'total': 'total',
|
||||
'datetime': 'datetime', 'status': 'status', 'pcnt': 'pcnt',
|
||||
'event': 'event'}
|
||||
|
||||
organizer = forms.ModelChoiceField(
|
||||
label=_('Organizer'),
|
||||
queryset=Organizer.objects.none(),
|
||||
@@ -173,6 +207,7 @@ class OrderSearchFilterForm(OrderFilterForm):
|
||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||
pk__in=request.user.teams.values_list('organizer', flat=True)
|
||||
)
|
||||
self.fields['provider'].choices += get_all_payment_providers()
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
@@ -187,7 +222,8 @@ class OrderSearchFilterForm(OrderFilterForm):
|
||||
class SubEventFilterForm(FilterForm):
|
||||
orders = {
|
||||
'date_from': 'date_from',
|
||||
'active': 'active'
|
||||
'active': 'active',
|
||||
'sum_quota_available': 'sum_quota_available'
|
||||
}
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
@@ -248,7 +284,8 @@ class EventFilterForm(FilterForm):
|
||||
'organizer': 'organizer__name',
|
||||
'date_from': 'order_from',
|
||||
'date_to': 'order_to',
|
||||
'live': 'live'
|
||||
'live': 'live',
|
||||
'sum_tickets_paid': 'sum_tickets_paid'
|
||||
}
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
|
||||
@@ -12,6 +12,7 @@ from pretix.base.models import (
|
||||
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
from pretix.control.forms import SplitDateTimePickerWidget
|
||||
|
||||
|
||||
class CategoryForm(I18nModelForm):
|
||||
@@ -47,7 +48,9 @@ class QuestionForm(I18nModelForm):
|
||||
'items'
|
||||
]
|
||||
widgets = {
|
||||
'items': forms.CheckboxSelectMultiple,
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -149,14 +152,18 @@ class ItemCreateForm(I18nModelForm):
|
||||
)
|
||||
|
||||
if not self.event.has_subevents:
|
||||
choices = [
|
||||
(self.NONE, _("Do not add to a quota now")),
|
||||
(self.EXISTING, _("Add product to an existing quota")),
|
||||
(self.NEW, _("Create a new quota for this product"))
|
||||
]
|
||||
if not self.event.quotas.exists():
|
||||
choices.remove(choices[1])
|
||||
|
||||
self.fields['quota_option'] = forms.ChoiceField(
|
||||
label=_("Quota options"),
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
(self.NONE, _("Do not add to a quota now")),
|
||||
(self.EXISTING, _("Add product to an existing quota")),
|
||||
(self.NEW, _("Create a new quota for this product"))
|
||||
),
|
||||
choices=choices,
|
||||
initial=self.NONE,
|
||||
required=False
|
||||
)
|
||||
@@ -178,7 +185,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
self.fields['quota_add_new_size'] = forms.IntegerField(
|
||||
min_value=0,
|
||||
label=_("Size"),
|
||||
widget=forms.TextInput(attrs={'placeholder': _("New quota size")}),
|
||||
widget=forms.TextInput(attrs={'placeholder': _("Number of tickets")}),
|
||||
help_text=_("Leave empty for an unlimited number of tickets."),
|
||||
required=False
|
||||
)
|
||||
@@ -263,6 +270,11 @@ class ItemUpdateForm(I18nModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['category'].queryset = self.instance.event.categories.all()
|
||||
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
||||
self.fields['description'].widget.attrs['placeholder'] = _(
|
||||
'e.g. This reduced price is available for full-time students, jobless and people '
|
||||
'over 65. This ticket includes access to all parts of the event, except the VIP '
|
||||
'area.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -286,9 +298,13 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'min_per_order',
|
||||
'checkin_attention'
|
||||
]
|
||||
field_classes = {
|
||||
'available_from': forms.SplitDateTimeField,
|
||||
'available_until': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'available_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'available_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ from pretix.base.services.pricing import get_price
|
||||
|
||||
|
||||
class ExtendForm(I18nModelForm):
|
||||
quota_ignore = forms.BooleanField(
|
||||
label=_('Overbook quota'),
|
||||
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
|
||||
'and you having sold more tickets than you planned!'),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['expires']
|
||||
@@ -25,6 +32,11 @@ class ExtendForm(I18nModelForm):
|
||||
})
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.status == Order.STATUS_PENDING or self.instance._is_still_available(now(), count_waitinglist=False) is True:
|
||||
del self.fields['quota_ignore']
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
data['expires'] = data['expires'].replace(hour=23, minute=59, second=59)
|
||||
@@ -78,6 +90,14 @@ class OtherOperationsForm(forms.Form):
|
||||
'Use with care and only if you need to. Note that rounding differences might occur in this procedure.'
|
||||
)
|
||||
)
|
||||
notify = forms.BooleanField(
|
||||
label=_('Notify user'),
|
||||
required=False,
|
||||
initial=True,
|
||||
help_text=_(
|
||||
'Send an email to the customer notifying that their order has been changed.'
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('order')
|
||||
@@ -167,7 +187,8 @@ class OrderPositionChangeForm(forms.Form):
|
||||
('product', 'Change product'),
|
||||
('price', 'Change price'),
|
||||
('subevent', 'Change event date'),
|
||||
('cancel', 'Remove product')
|
||||
('cancel', 'Remove product'),
|
||||
('split', 'Split into new order'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
@@ -8,6 +9,7 @@ from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.models import Organizer, Team
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
|
||||
class OrganizerForm(I18nModelForm):
|
||||
@@ -66,7 +68,9 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
|
||||
elif current_domain:
|
||||
current_domain.delete()
|
||||
instance.get_cache().clear()
|
||||
instance.cache.clear()
|
||||
for ev in instance.events.all():
|
||||
ev.cache.clear()
|
||||
|
||||
return instance
|
||||
|
||||
@@ -113,20 +117,6 @@ class TeamForm(forms.ModelForm):
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
locales = forms.MultipleChoiceField(
|
||||
choices=settings.LANGUAGES,
|
||||
label=_("Use languages"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=_('Choose all languages that your organizer homepage should be available in.')
|
||||
)
|
||||
|
||||
organizer_homepage_text = I18nFormField(
|
||||
label=_('Homepage text'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_('This will be displayed on the organizer homepage.')
|
||||
)
|
||||
|
||||
organizer_info_text = I18nFormField(
|
||||
label=_('Info text'),
|
||||
required=False,
|
||||
@@ -134,6 +124,23 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
|
||||
)
|
||||
|
||||
|
||||
class OrganizerDisplaySettingsForm(SettingsForm):
|
||||
primary_color = forms.CharField(
|
||||
label=_("Primary color"),
|
||||
required=False,
|
||||
validators=[
|
||||
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
|
||||
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
|
||||
],
|
||||
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
|
||||
)
|
||||
organizer_homepage_text = I18nFormField(
|
||||
label=_('Homepage text'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_('This will be displayed on the organizer homepage.')
|
||||
)
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
@@ -141,7 +148,6 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
)
|
||||
|
||||
event_list_type = forms.ChoiceField(
|
||||
label=_('Default overview style'),
|
||||
choices=(
|
||||
@@ -149,3 +155,22 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
('calendar', _('Calendar'))
|
||||
)
|
||||
)
|
||||
locales = forms.MultipleChoiceField(
|
||||
choices=settings.LANGUAGES,
|
||||
label=_("Use languages"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=_('Choose all languages that your organizer homepage should be available in.')
|
||||
)
|
||||
primary_font = forms.ChoiceField(
|
||||
label=_('Font'),
|
||||
choices=[
|
||||
('Open Sans', 'Open Sans')
|
||||
],
|
||||
help_text=_('Only respected by modern browsers.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['primary_font'].choices += [
|
||||
(a, a) for a in get_fonts()
|
||||
]
|
||||
|
||||
54
src/pretix/control/forms/renderers.py
Normal file
54
src/pretix/control/forms/renderers.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from bootstrap3.renderers import FieldRenderer
|
||||
from bootstrap3.text import text_value
|
||||
from django.forms import CheckboxInput
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext
|
||||
from i18nfield.forms import I18nFormField
|
||||
|
||||
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', optional=False):
|
||||
"""
|
||||
Render a label with content
|
||||
"""
|
||||
attrs = {}
|
||||
if label_for:
|
||||
attrs['for'] = label_for
|
||||
if label_class:
|
||||
attrs['class'] = label_class
|
||||
if label_title:
|
||||
attrs['title'] = label_title
|
||||
builder = '<{tag}{attrs}>{content}{opt}</{tag}>'
|
||||
return format_html(
|
||||
builder,
|
||||
tag='label',
|
||||
attrs=mark_safe(flatatt(attrs)) if attrs else '',
|
||||
opt=mark_safe('<br><span class="optional">{}</span>'.format(pgettext('form', 'Optional'))) if optional else '',
|
||||
content=text_value(content),
|
||||
)
|
||||
|
||||
|
||||
class ControlFieldRenderer(FieldRenderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = 'horizontal'
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def add_label(self, html):
|
||||
label = self.get_label()
|
||||
|
||||
if hasattr(self.field.field, '_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._required
|
||||
elif isinstance(self.field.field, I18nFormField):
|
||||
required = self.field.field.one_required
|
||||
else:
|
||||
required = self.field.field.required
|
||||
|
||||
html = render_label(
|
||||
label,
|
||||
label_for=self.field.id_for_label,
|
||||
label_class=self.get_label_class(),
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput)
|
||||
) + html
|
||||
return html
|
||||
@@ -5,6 +5,7 @@ from i18nfield.forms import I18nInlineFormSet
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.control.forms import SplitDateTimePickerWidget
|
||||
|
||||
|
||||
class SubEventForm(I18nModelForm):
|
||||
@@ -27,13 +28,19 @@ class SubEventForm(I18nModelForm):
|
||||
'location',
|
||||
'frontpage_text'
|
||||
]
|
||||
field_classes = {
|
||||
'date_from': forms.SplitDateTimeField,
|
||||
'date_to': forms.SplitDateTimeField,
|
||||
'date_admission': forms.SplitDateTimeField,
|
||||
'presale_start': forms.SplitDateTimeField,
|
||||
'presale_end': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
|
||||
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_presale_start'}),
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ import copy
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
||||
from pretix.control.forms import SplitDateTimePickerWidget
|
||||
from pretix.control.signals import voucher_form_validation
|
||||
|
||||
|
||||
@@ -27,8 +26,11 @@ class VoucherForm(I18nModelForm):
|
||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||
'comment', 'max_usages', 'price_mode', 'subevent'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'valid_until': SplitDateTimePickerWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -88,113 +90,41 @@ class VoucherForm(I18nModelForm):
|
||||
self.instance.variation = None
|
||||
self.instance.quota = None
|
||||
|
||||
if self.instance.item.category and self.instance.item.category.is_addon:
|
||||
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
||||
else:
|
||||
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
|
||||
if data.get('max_usages', 0) < self.instance.redeemed:
|
||||
raise ValidationError(
|
||||
_('This voucher has already been redeemed %(redeemed)s times. You cannot reduce the maximum number of '
|
||||
'usages below this number.'),
|
||||
params={
|
||||
'redeemed': self.instance.redeemed
|
||||
}
|
||||
)
|
||||
|
||||
if 'codes' in data:
|
||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||
cnt = len(data['codes']) * data['max_usages']
|
||||
else:
|
||||
cnt = data['max_usages']
|
||||
|
||||
if self.instance.event.has_subevents and data['block_quota'] and not data.get('subevent'):
|
||||
raise ValidationError(pgettext_lazy(
|
||||
'subevent',
|
||||
'If you want this voucher to block quota, you need to select a specific date.'
|
||||
))
|
||||
|
||||
if self._clean_quota_needs_checking(data):
|
||||
self._clean_quota_check(data, cnt)
|
||||
|
||||
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=self.instance.event) & ~Q(pk=self.instance.pk)).exists():
|
||||
raise ValidationError(_('A voucher with this code already exists.'))
|
||||
Voucher.clean_item_properties(
|
||||
data, self.instance.event,
|
||||
self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_subevent(
|
||||
data, self.instance.event
|
||||
)
|
||||
Voucher.clean_max_usages(data, self.instance.redeemed)
|
||||
check_quota = Voucher.clean_quota_needs_checking(
|
||||
data, self.initial_instance_data,
|
||||
item_changed=data.get('itemvar') != self.initial.get('itemvar'),
|
||||
creating=not self.instance.pk
|
||||
)
|
||||
if check_quota:
|
||||
Voucher.clean_quota_check(
|
||||
data, cnt, self.initial_instance_data, self.instance.event,
|
||||
self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||
|
||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
||||
|
||||
return data
|
||||
|
||||
def _clean_quota_needs_checking(self, data):
|
||||
# We only need to check for quota on vouchers that are now blocking quota and haven't
|
||||
# before (or have blocked a different quota before)
|
||||
if data.get('block_quota', False):
|
||||
is_valid = data.get('valid_until') is None or data.get('valid_until') >= now()
|
||||
if not is_valid:
|
||||
# If the voucher is not valid, it won't block any quota
|
||||
return False
|
||||
|
||||
if not self.instance.pk:
|
||||
# This is a new voucher
|
||||
return True
|
||||
|
||||
if not self.initial_instance_data.block_quota:
|
||||
# Change from nonblocking to blocking
|
||||
return True
|
||||
|
||||
if not self._clean_was_valid():
|
||||
# This voucher has been expired and is now valid again and therefore blocks quota again
|
||||
return True
|
||||
|
||||
if data.get('itemvar') != self.initial.get('itemvar'):
|
||||
# The voucher has been reassigned to a different item, variation or quota
|
||||
return True
|
||||
|
||||
if data.get('subevent') != self.initial.get('subevent'):
|
||||
# The voucher has been reassigned to a different subevent
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _clean_was_valid(self):
|
||||
return self.initial_instance_data.valid_until is None or self.initial_instance_data.valid_until >= now()
|
||||
|
||||
def _clean_quota_get_ignored(self):
|
||||
quotas = set()
|
||||
if self.initial_instance_data and self.initial_instance_data.block_quota and self._clean_was_valid():
|
||||
if self.initial_instance_data.quota:
|
||||
quotas.add(self.initial_instance_data.quota)
|
||||
elif self.initial_instance_data.variation:
|
||||
quotas |= set(self.initial_instance_data.variation.quotas.filter(
|
||||
subevent=self.initial_instance_data.subevent))
|
||||
elif self.initial_instance_data.item:
|
||||
quotas |= set(self.initial_instance_data.item.quotas.filter(
|
||||
subevent=self.initial_instance_data.subevent))
|
||||
return quotas
|
||||
|
||||
def _clean_quota_check(self, data, cnt):
|
||||
old_quotas = self._clean_quota_get_ignored()
|
||||
|
||||
if self.instance.quota:
|
||||
if self.instance.quota in old_quotas:
|
||||
return
|
||||
else:
|
||||
avail = self.instance.quota.availability()
|
||||
elif self.instance.item and self.instance.item.has_variations and not self.instance.variation:
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
elif self.instance.item and self.instance.variation:
|
||||
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
elif self.instance.item and not self.instance.item.has_variations:
|
||||
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
else:
|
||||
raise ValidationError(_('You need to specify either a quota or a product.'))
|
||||
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
|
||||
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
|
||||
'quota is currently sold out or completely reserved.'))
|
||||
|
||||
def save(self, commit=True):
|
||||
super().save(commit)
|
||||
|
||||
@@ -218,8 +148,11 @@ class VoucherBulkForm(VoucherForm):
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||
'max_usages', 'price_mode', 'subevent'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'valid_until': SplitDateTimePickerWidget(),
|
||||
}
|
||||
labels = {
|
||||
'max_usages': _('Maximum usages per voucher')
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.dispatch import receiver
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -82,13 +85,28 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
price=formats.localize(Decimal(data['price'])),
|
||||
currency=event.currency
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.split':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) split into new order: {order}').format(
|
||||
old_item=old_item,
|
||||
posid=data.get('positionid', '?'),
|
||||
order=data['new_order'],
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
currency=event.currency
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.split_from':
|
||||
return _('This order has been created by splitting the order {order}').format(
|
||||
order=data['original_order'],
|
||||
)
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
plains = {
|
||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||
'pretix.event.order.modified': _('The order details have been modified.'),
|
||||
'pretix.event.order.modified': _('The order details have been changed.'),
|
||||
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
|
||||
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
|
||||
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
|
||||
@@ -108,12 +126,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
|
||||
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
|
||||
'to expire.'),
|
||||
'pretix.event.order.email.order_canceled': _('An email has been sent to notify user order has been canceled.'),
|
||||
'pretix.event.order.email.order_changed': _('An email has been sent to notify user order has been changed.'),
|
||||
'pretix.event.order.email.order_free': _('An email has been sent to notify user order has been received.'),
|
||||
'pretix.event.order.email.order_paid': _('An email has been sent to notify user payment has been received.'),
|
||||
'pretix.event.order.email.order_placed': _('An email has been sent to notify user order has been received and require payment.'),
|
||||
'pretix.event.order.email.resend': _('An email has been sent with link to the order detail page has been resent to the user.'),
|
||||
'pretix.event.order.email.order_canceled': _('An email has been sent to notify the user that the order has been canceled.'),
|
||||
'pretix.event.order.email.order_changed': _('An email has been sent to notify the user that the order has been changed.'),
|
||||
'pretix.event.order.email.order_free': _('An email has been sent to notify the user that the order has been received.'),
|
||||
'pretix.event.order.email.order_paid': _('An email has been sent to notify the user that payment has been received.'),
|
||||
'pretix.event.order.email.order_placed': _('An email has been sent to notify the user that the order has been received and requires payment.'),
|
||||
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
|
||||
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
|
||||
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
|
||||
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
|
||||
@@ -125,27 +143,27 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
|
||||
'pretix.voucher.added': _('The voucher has been created.'),
|
||||
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
|
||||
'pretix.voucher.changed': _('The voucher has been modified.'),
|
||||
'pretix.voucher.changed': _('The voucher has been changed.'),
|
||||
'pretix.voucher.deleted': _('The voucher has been deleted.'),
|
||||
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
|
||||
'pretix.event.item.added': _('The product has been created.'),
|
||||
'pretix.event.item.changed': _('The product has been modified.'),
|
||||
'pretix.event.item.changed': _('The product has been changed.'),
|
||||
'pretix.event.item.deleted': _('The product has been deleted.'),
|
||||
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
|
||||
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
|
||||
'pretix.event.item.variation.changed': _('The variation "{value}" has been modified.'),
|
||||
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
|
||||
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
|
||||
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
|
||||
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
|
||||
'pretix.event.quota.added': _('The quota has been added.'),
|
||||
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
||||
'pretix.event.quota.changed': _('The quota has been modified.'),
|
||||
'pretix.event.quota.changed': _('The quota has been changed.'),
|
||||
'pretix.event.category.added': _('The category has been added.'),
|
||||
'pretix.event.category.deleted': _('The category has been deleted.'),
|
||||
'pretix.event.category.changed': _('The category has been modified.'),
|
||||
'pretix.event.category.changed': _('The category has been changed.'),
|
||||
'pretix.event.question.added': _('The question has been added.'),
|
||||
'pretix.event.question.deleted': _('The question has been deleted.'),
|
||||
'pretix.event.question.changed': _('The question has been modified.'),
|
||||
'pretix.event.question.changed': _('The question has been changed.'),
|
||||
'pretix.event.taxrule.added': _('The tax rule has been added.'),
|
||||
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
|
||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||
@@ -165,13 +183,13 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
'pretix.team.changed': _('The team settings have been modified.'),
|
||||
'pretix.team.changed': _('The team settings have been changed.'),
|
||||
'pretix.team.deleted': _('The team has been deleted.'),
|
||||
'pretix.subevent.deleted': pgettext_lazy('subevent', 'The event date has been deleted.'),
|
||||
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been modified.'),
|
||||
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been changed.'),
|
||||
'pretix.subevent.added': pgettext_lazy('subevent', 'The event date has been created.'),
|
||||
'pretix.subevent.quota.added': pgettext_lazy('subevent', 'A quota has been added to the event date.'),
|
||||
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been modified on the event date.'),
|
||||
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been changed on the event date.'),
|
||||
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
|
||||
}
|
||||
|
||||
@@ -200,6 +218,21 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
|
||||
return _('The settings of a ticket output provider have been changed.')
|
||||
|
||||
if logentry.action_type == 'pretix.control.views.checkin':
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been checked in manually at {datetime}.').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
return _('Position #{posid} has been checked in again at {datetime}.').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.added':
|
||||
return _('{user} has been added to the team.').format(user=data.get('email'))
|
||||
|
||||
|
||||
@@ -64,17 +64,15 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
return self._login_redirect(request)
|
||||
|
||||
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
|
||||
# If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py
|
||||
last_used = request.session.get('pretix_auth_last_used', time.time())
|
||||
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
|
||||
logout(request)
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
return self._login_redirect(request)
|
||||
if url_name != 'user.reauth':
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE and url_name != 'user.reauth':
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
request.event = Event.objects.filter(
|
||||
|
||||
@@ -181,3 +181,43 @@ and your tempalte inherits from ``pretixcontrol/organizers/base.html``.
|
||||
This is a regular django signal (no pretix event signal). Receivers will be passed
|
||||
the keyword arguments ``organizer`` and ``request``.
|
||||
"""
|
||||
|
||||
order_info = EventPluginSignal(
|
||||
providing_args=["order", "request"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to display additional information on the order detail page
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
"""
|
||||
|
||||
|
||||
nav_event_settings = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
)
|
||||
"""
|
||||
This signal is sent out to include tab links on the settings page of an event.
|
||||
Receivers are expected to return a list of dictionaries. The dictionaries
|
||||
should contain at least the keys ``label`` and ``url``. You should also return
|
||||
an ``active`` key with a boolean set to ``True``, when this item should be marked
|
||||
as active.
|
||||
|
||||
If your linked view should stay in the tab-like context of this page, we recommend
|
||||
that you use ``pretix.control.views.event.EventSettingsViewMixin`` for your view
|
||||
and your tempalte inherits from ``pretixcontrol/event/settings_base.html``.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
A second keyword argument ``request`` will contain the request object.
|
||||
"""
|
||||
|
||||
event_settings_widget = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
)
|
||||
"""
|
||||
This signal is sent out to include template snippets on the settings page of an event
|
||||
that allows generating a pretix Widget code.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
A second keyword argument ``request`` will contain the request object.
|
||||
"""
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
|
||||
@@ -74,6 +75,11 @@
|
||||
{% for e in entries %}
|
||||
{% with e.checkins.first as checkin %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="checkin"
|
||||
id="id_checkin" class=""
|
||||
value="{{e.pk}}"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
|
||||
</td>
|
||||
@@ -107,6 +113,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Check-In selected attendees" %}
|
||||
</button>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.index" %}class="active"{% endif %}>
|
||||
<i class="fa fa-dashboard fa-fw"></i>
|
||||
{% trans "Dashboard" %}
|
||||
{% trans "Event dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
|
||||
{% if is_event_settings or "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
|
||||
<i class="fa fa-wrench fa-fw"></i>
|
||||
{% trans "Settings" %}
|
||||
</a>
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
<p>{{ text }}</p>
|
||||
{% endif %}
|
||||
{% if button_text %}
|
||||
<p><a href="{{ button_url }}" class="btn btn-default btn-lg">{{ button_text }}</a></p>
|
||||
<p><a href="{{ button_url }}" class="btn btn-primary btn-lg">{{ button_text }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,17 +1,24 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load hierarkey_form %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_field form.primary_color layout="horizontal" %}
|
||||
{% bootstrap_field form.primary_font layout="horizontal" %}
|
||||
{% bootstrap_field form.logo_image layout="horizontal" %}
|
||||
{% bootstrap_field form.frontpage_text layout="horizontal" %}
|
||||
{% bootstrap_field form.show_variations_expanded layout="horizontal" %}
|
||||
<legend>{% trans "Event page" %}</legend>
|
||||
{% bootstrap_field form.logo_image layout="control" %}
|
||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||
{% bootstrap_field form.show_variations_expanded layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
{% url "control:organizer.display" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "primary_color" "primary_font" %}
|
||||
{% bootstrap_field form.primary_color layout="control" %}
|
||||
{% bootstrap_field form.primary_font layout="control" %}
|
||||
{% endpropagated %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Invoicing" %}</legend>
|
||||
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_address_required layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_name_required layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_generate layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_address_vatid layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_numbers_consecutive layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_numbers_prefix layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_renderer layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_language layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_include_free layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_address_from layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_introductory_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_additional_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_logo_image layout="horizontal" %}
|
||||
{% bootstrap_field form.invoice_address_asked layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_required layout="control" %}
|
||||
{% bootstrap_field form.invoice_name_required layout="control" %}
|
||||
{% bootstrap_field form.invoice_generate layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_vatid layout="control" %}
|
||||
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
|
||||
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
|
||||
{% bootstrap_field form.invoice_renderer layout="control" %}
|
||||
{% bootstrap_field form.invoice_language layout="control" %}
|
||||
{% bootstrap_field form.invoice_include_free layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_from layout="control" %}
|
||||
{% bootstrap_field form.invoice_introductory_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_logo_image layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail settings" %}</legend>
|
||||
{% bootstrap_field form.mail_prefix layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_from layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_prefix layout="control" %}
|
||||
{% bootstrap_field form.mail_from layout="control" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail content" %}</legend>
|
||||
@@ -48,13 +48,13 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "SMTP settings" %}</legend>
|
||||
{% bootstrap_field form.smtp_use_custom layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_host layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_port layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_username layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_password layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_use_tls layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_use_ssl layout="horizontal" %}
|
||||
{% bootstrap_field form.smtp_use_custom layout="control" %}
|
||||
{% bootstrap_field form.smtp_host layout="control" %}
|
||||
{% bootstrap_field form.smtp_port layout="control" %}
|
||||
{% bootstrap_field form.smtp_username layout="control" %}
|
||||
{% bootstrap_field form.smtp_password layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_tls layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_ssl layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Payment settings" %}</legend>
|
||||
{% bootstrap_field sform.payment_term_days layout="horizontal" %}
|
||||
{% bootstrap_field sform.payment_term_last layout="horizontal" %}
|
||||
{% bootstrap_field sform.payment_term_weekdays layout="horizontal" %}
|
||||
{% bootstrap_field sform.payment_term_expire_automatically layout="horizontal" %}
|
||||
{% bootstrap_field sform.payment_term_accept_late layout="horizontal" %}
|
||||
{% bootstrap_field sform.tax_rate_default layout="horizontal" %}
|
||||
{% bootstrap_field sform.payment_term_days layout="control" %}
|
||||
{% bootstrap_field sform.payment_term_last layout="control" %}
|
||||
{% bootstrap_field sform.payment_term_weekdays layout="control" %}
|
||||
{% bootstrap_field sform.payment_term_expire_automatically layout="control" %}
|
||||
{% bootstrap_field sform.payment_term_accept_late layout="control" %}
|
||||
{% bootstrap_field sform.tax_rate_default layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Payment providers" %}</legend>
|
||||
@@ -38,7 +38,7 @@
|
||||
</div>
|
||||
<div id="{{ provider.identifier }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
{% bootstrap_form provider.form layout='horizontal' %}
|
||||
{% bootstrap_form provider.form layout='control' %}
|
||||
{% with c=provider.settings_content %}
|
||||
{% if c %}{{ c|safe }}{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.location layout="horizontal" %}
|
||||
{% bootstrap_field form.date_admission layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
{% bootstrap_field form.is_public layout="horizontal" %}
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.slug layout="control" %}
|
||||
{% bootstrap_field form.date_from layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
|
||||
{% bootstrap_field form.date_to layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
{% bootstrap_field form.date_admission layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
|
||||
{% bootstrap_field form.currency layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
|
||||
{% if meta_forms %}
|
||||
<div class="form-group metadata-group">
|
||||
@@ -38,40 +38,41 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_field sform.locales layout="horizontal" %}
|
||||
{% bootstrap_field sform.locale layout="horizontal" %}
|
||||
{% bootstrap_field sform.timezone layout="horizontal" %}
|
||||
{% bootstrap_field sform.show_date_to layout="horizontal" %}
|
||||
{% bootstrap_field sform.show_times layout="horizontal" %}
|
||||
{% bootstrap_field sform.contact_mail layout="horizontal" %}
|
||||
{% bootstrap_field sform.imprint_url layout="horizontal" %}
|
||||
{% bootstrap_field sform.show_quota_left layout="horizontal" %}
|
||||
{% bootstrap_field sform.display_net_prices layout="horizontal" %}
|
||||
{% bootstrap_field sform.locales layout="control" %}
|
||||
{% bootstrap_field sform.locale layout="control" %}
|
||||
{% bootstrap_field sform.timezone layout="control" %}
|
||||
{% bootstrap_field sform.show_date_to layout="control" %}
|
||||
{% bootstrap_field sform.show_times layout="control" %}
|
||||
{% bootstrap_field sform.contact_mail layout="control" %}
|
||||
{% bootstrap_field sform.imprint_url layout="control" %}
|
||||
{% bootstrap_field sform.confirm_text layout="control" %}
|
||||
{% bootstrap_field sform.show_quota_left layout="control" %}
|
||||
{% bootstrap_field sform.display_net_prices layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="horizontal" %}
|
||||
{% bootstrap_field sform.presale_start_show_date layout="horizontal" %}
|
||||
{% bootstrap_field form.presale_end layout="horizontal" %}
|
||||
{% bootstrap_field sform.show_items_outside_presale_period layout="horizontal" %}
|
||||
{% bootstrap_field sform.last_order_modification_date layout="horizontal" %}
|
||||
{% bootstrap_field form.presale_start layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
|
||||
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
||||
{% bootstrap_field form.presale_end layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
|
||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Orders" %}</legend>
|
||||
{% bootstrap_field sform.reservation_time layout="horizontal" %}
|
||||
{% bootstrap_field sform.max_items_per_order layout="horizontal" %}
|
||||
{% bootstrap_field sform.attendee_names_asked layout="horizontal" %}
|
||||
{% bootstrap_field sform.attendee_names_required layout="horizontal" %}
|
||||
{% bootstrap_field sform.order_email_asked_twice layout="horizontal" %}
|
||||
{% bootstrap_field sform.attendee_emails_asked layout="horizontal" %}
|
||||
{% bootstrap_field sform.attendee_emails_required layout="horizontal" %}
|
||||
{% bootstrap_field sform.cancel_allow_user layout="horizontal" %}
|
||||
{% bootstrap_field sform.reservation_time layout="control" %}
|
||||
{% bootstrap_field sform.max_items_per_order layout="control" %}
|
||||
{% bootstrap_field sform.attendee_names_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_names_required layout="control" %}
|
||||
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_required layout="control" %}
|
||||
{% bootstrap_field sform.cancel_allow_user layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Waiting list" %}</legend>
|
||||
{% bootstrap_field sform.waiting_list_enabled layout="horizontal" %}
|
||||
{% bootstrap_field sform.waiting_list_auto layout="horizontal" %}
|
||||
{% bootstrap_field sform.waiting_list_hours layout="horizontal" %}
|
||||
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_hours layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{{ request.event.name }}{% endblock %}
|
||||
{% block content %}
|
||||
{% if "congratulations" in request.GET %}
|
||||
<div class="thank-you">
|
||||
<span class="fa fa-check-circle"></span>
|
||||
|
||||
<h2>{% trans "Congratulations!" %}</h2>
|
||||
<p>
|
||||
<strong>{% trans "You just created an event!" %}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can now scroll down and modify the settings in more detail, if you want, or you can create your
|
||||
first product to start selling tickets right away!
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{% url "control:event.items.add" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default">
|
||||
{% trans "Create a first product" %}
|
||||
</a>
|
||||
</p>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1>{% trans "Settings" %}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
@@ -51,7 +75,19 @@
|
||||
{% trans "Permissions" %}
|
||||
</a>
|
||||
</li>
|
||||
<li {% if "event.settings.widget" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.settings.widget' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Widget" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for nav in nav_event_settings %}
|
||||
<li {% if nav.active %}class="active"{% endif %}>
|
||||
<a href="{{ nav.url }}">
|
||||
{{ nav.label }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.rate layout="horizontal" %}
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
<div class="alert alert-warning">
|
||||
<span class="fa fa-w fa-legal fa-4x pull-left"></span>
|
||||
@@ -29,9 +29,9 @@
|
||||
{% endblocktrans %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% bootstrap_field form.price_includes_tax layout="horizontal" %}
|
||||
{% bootstrap_field form.eu_reverse_charge layout="horizontal" %}
|
||||
{% bootstrap_field form.home_country layout="horizontal" %}
|
||||
{% bootstrap_field form.price_includes_tax layout="control" %}
|
||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||
{% bootstrap_field form.home_country layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.ticket_download layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_date layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_addons layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_nonadm layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download layout="control" %}
|
||||
{% bootstrap_field form.ticket_download_date layout="control" %}
|
||||
{% bootstrap_field form.ticket_download_addons layout="control" %}
|
||||
{% bootstrap_field form.ticket_download_nonadm layout="control" %}
|
||||
{% for provider in providers %}
|
||||
<div class="panel panel-default ticketoutput-panel">
|
||||
<div class="panel-heading">
|
||||
|
||||
71
src/pretix/control/templates/pretixcontrol/event/widget.html
Normal file
71
src/pretix/control/templates/pretixcontrol/event/widget.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventurl %}
|
||||
{% load eventsignal %}
|
||||
{% block inside %}
|
||||
<legend>{% trans "Widget" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The pretix widget is a way to embed your ticket shop into your event website. This way, your visitors can
|
||||
buy their ticket right away without leaving your website.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if valid %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
To embed the widget onto your website, simply copy the following code to the <code><head></code>
|
||||
section of your website:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<pre><link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}">
|
||||
<script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async></script></pre>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Then, copy the following code to the place of your website where you want the widget to show up:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if form.cleaned_data.subevent %}
|
||||
{% abseventurl request.event "presale:event.index" subevent=form.cleaned_data.subevent.pk as indexurl %}
|
||||
{% else %}
|
||||
{% abseventurl request.event "presale:event.index" as indexurl %}
|
||||
{% endif %}
|
||||
<pre><pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
{% blocktrans trimmed with a_attr='target="_blank" href="'|add:indexurl|add:'"'|safe %}
|
||||
JavaScript is disabled in your browser. To access our ticket shop without JavaScript,
|
||||
please <a {{ a_attr }}>click here</a>.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</pre>
|
||||
<p>
|
||||
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank">
|
||||
<span class="fa fa-question-circle"></span>
|
||||
{% trans "Read our documentation for more information" %}
|
||||
</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Using this form, you can generate a code to copy and paste to your website source.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-3 col-md-9">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Generate widget code" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% eventsignal request.event "pretix.control.signals.event_settings_widget" request=request %}
|
||||
{% endblock %}
|
||||
@@ -14,15 +14,15 @@
|
||||
{% block form %}
|
||||
{% endblock %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save pull-right">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
|
||||
class="btn btn-default btn-lg pull-left">
|
||||
{% trans "Back" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user