Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
dfa30e0a49 Bump version 2017-09-05 15:35:38 +02:00
Raphael Michel
5ee0c84a46 Re-do squashed migration 2017-09-05 15:35:19 +02:00
258 changed files with 4684 additions and 23892 deletions

1
.gitattributes vendored
View File

@@ -5,7 +5,6 @@ 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

View File

@@ -12,29 +12,29 @@ services:
- postgresql
matrix:
include:
- python: 3.6
- 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-cov
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.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
- python: 3.6
env: JOB=tests-cov
addons:
postgresql: "9.4"

View File

@@ -44,7 +44,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -90,7 +90,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,

View File

@@ -54,7 +54,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -103,7 +103,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"name": {"en": "Sample Conference"},

View File

@@ -41,7 +41,6 @@ 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.
===================================== ========================== =======================================================
@@ -58,11 +57,6 @@ internal_reference string Customer's refe
``foreign_currency_rate_date`` have been added.
.. versionchanged:: 1.9
The attribute ``internal_reference`` has been added.
Endpoints
---------
@@ -84,7 +78,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -101,7 +95,6 @@ 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",
@@ -153,7 +146,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"number": "SAMPLECONF-00001",
@@ -165,7 +158,6 @@ 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",

View File

@@ -101,7 +101,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -188,7 +188,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,

View File

@@ -43,7 +43,6 @@ 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
@@ -81,10 +80,6 @@ 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
-----------------------
@@ -146,7 +141,7 @@ Order endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -175,7 +170,6 @@ Order endpoints
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": False
},
@@ -257,7 +251,7 @@ Order endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"code": "ABC12",
@@ -281,7 +275,6 @@ Order endpoints
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": False
},
@@ -336,7 +329,6 @@ 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)/
@@ -375,206 +367,9 @@ 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
------------------------
@@ -597,7 +392,7 @@ Order position endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -681,7 +476,7 @@ Order position endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 23442,
@@ -725,7 +520,6 @@ 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)/
@@ -765,6 +559,5 @@ 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.

View File

@@ -41,7 +41,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -77,7 +77,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"name": "Big Events LLC",

View File

@@ -54,7 +54,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -113,7 +113,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,

View File

@@ -42,7 +42,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -88,7 +88,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,
@@ -124,7 +124,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"available": true,

View File

@@ -60,7 +60,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -114,7 +114,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,

View File

@@ -25,10 +25,6 @@ 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
---------
@@ -51,7 +47,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -94,7 +90,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,
@@ -107,128 +103,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the tax rule to fetch
:param id: The ``slug`` field of the sub-event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
: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.
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -44,10 +44,6 @@ subevent integer ID of the date
===================================== ========================== =======================================================
.. versionchanged:: 1.9
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
Endpoints
---------
@@ -69,7 +65,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -140,7 +136,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,
@@ -166,152 +162,3 @@ 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.

View File

@@ -48,7 +48,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"count": 1,
@@ -102,7 +102,7 @@ Endpoints
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
Content-Type: text/javascript
{
"id": 1,

View File

@@ -60,85 +60,7 @@ your views::
def admin_view(request, organizer, event):
...
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 %}
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``.
Frontend views
--------------

View File

@@ -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_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
:members: validate_cart, order_paid, order_placed
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, question_form_fields, checkout_confirm_messages
.. 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, nav_event_settings, order_info, event_settings_widget
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display
:members: logentry_display, requiredaction_display
Vouchers
""""""""
@@ -64,9 +64,3 @@ Dashboards
.. automodule:: pretix.control.signals
:members: event_dashboard_widgets, user_dashboard_widgets
Ticket designs
""""""""""""""
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: layout_text_variables

View File

@@ -114,19 +114,6 @@ 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
-----

View File

@@ -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 subtracting the following numbers from the quota
The availability of a quota is currently calculated by substracting 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

View File

@@ -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, 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, get_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

View File

@@ -20,6 +20,7 @@ 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``)
@@ -36,7 +37,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::
python3 -m venv env
pyvenv env
source env/bin/activate
You should now see a ``(env)`` prepended to your shell prompt. You have to do this

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,104 +0,0 @@
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/

View File

@@ -11,4 +11,3 @@ wanting to use pretix to sell tickets.
events/create
events/taxes
payments/index
events/widget

View File

@@ -1,17 +1,44 @@
<?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"
viewBox="0 0 149.59399 149.59399"
version="1.1"
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"
height="159.56693"
width="159.56693">
version="1.1"
inkscape:version="0.48.5 r10040"
sodipodi:docname="icon_draft.svg">
<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>
@@ -25,12 +52,15 @@
</rdf:RDF>
</metadata>
<g
transform="translate(-257.78125,-548.74975)"
id="layer1">
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-257.78125,-548.74975)">
<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"
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" />
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccccccccssscccccccccccccccccccsscscccccccssccccccccccccccccccccccccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -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 "data/*" --ignore "build/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "build/*"
staticfiles: jsi18n
./manage.py collectstatic --noinput

View File

@@ -1 +1 @@
__version__ = "1.9.0"
__version__ = "1.7.1"

View File

@@ -1,4 +1,3 @@
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
@@ -14,13 +13,6 @@ 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:
@@ -33,8 +25,9 @@ class EventPermission(BasePermission):
request.organizer = request.event.organizer
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if required_permission and required_permission not in request.eventpermset:
return False
if hasattr(view, 'permission'):
if view.permission and view.permission not in request.eventpermset:
return False
elif 'organizer' in request.resolver_match.kwargs:
request.organizer = Organizer.objects.filter(
@@ -44,21 +37,7 @@ class EventPermission(BasePermission):
return False
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if required_permission and required_permission not in request.orgapermset:
return False
if hasattr(view, 'permission'):
if view.permission and view.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

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,7 +1,5 @@
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
@@ -24,16 +22,6 @@ 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

View File

@@ -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', 'internal_reference')
'vat_id_validated')
class AnswerSerializer(I18nAwareModelSerializer):
@@ -153,5 +153,4 @@ 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',
'internal_reference')
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date')

View File

@@ -8,36 +8,3 @@ 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

View File

@@ -1,13 +1,11 @@
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):
@@ -38,39 +36,9 @@ class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
)
class TaxRuleViewSet(viewsets.ModelViewSet):
class TaxRuleViewSet(viewsets.ReadOnlyModelViewSet):
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)

View File

@@ -34,7 +34,6 @@ 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()
@@ -53,7 +52,6 @@ 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()
@@ -65,7 +63,6 @@ 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()
@@ -84,7 +81,6 @@ 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()

View File

@@ -1,28 +1,18 @@
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 serializers, status, viewsets
from rest_framework import 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, Quota
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models import Invoice, Order, OrderPosition
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,
)
@@ -44,7 +34,6 @@ 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(
@@ -82,130 +71,6 @@ 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')

View File

@@ -4,12 +4,10 @@ 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):
@@ -29,7 +27,7 @@ class VoucherFilter(FilterSet):
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class VoucherViewSet(viewsets.ModelViewSet):
class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = VoucherSerializer
queryset = Voucher.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -37,49 +35,6 @@ class VoucherViewSet(viewsets.ModelViewSet):
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)

View File

@@ -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, quotas # NOQA
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check # NOQA
try:
from .celery_app import app as celery_app # NOQA

View File

@@ -1,6 +1,6 @@
import hashlib
import time
from typing import Callable, Dict, List
from typing import Dict, List
from django.core.cache import caches
from django.db.models import Model
@@ -11,19 +11,17 @@ 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, known_prefix=None) -> str:
def _prefix_key(self, original_key: str) -> 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 = known_prefix or self.cache.get(self.prefixkey)
prefix = 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
@@ -34,25 +32,17 @@ 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=300):
def set(self, key: str, value: str, timeout: int=3600):
return self.cache.set(self._prefix_key(key), value, timeout)
def get(self, key: str) -> str:
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
)
return self.cache.get(self._prefix_key(key))
def get_many(self, keys: List[str]) -> Dict[str, str]:
values = self.cache.get_many([self._prefix_key(key) for key in keys])
@@ -61,7 +51,7 @@ class NamespacedCache:
newvalues[self._strip_prefix(k)] = v
return newvalues
def set_many(self, values: Dict[str, str], timeout=300):
def set_many(self, values: Dict[str, str], timeout=3600):
newvalues = {}
for k, v in values.items():
newvalues[self._prefix_key(k)] = v

View File

@@ -25,9 +25,7 @@ class AnswerFilesExporter(BaseExporter):
forms.ModelMultipleChoiceField(
queryset=self.event.questions.filter(type='F'),
label=_('Questions'),
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
widget=forms.CheckboxSelectMultiple,
required=False
)),
]
@@ -43,7 +41,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('rb')
i.file.open('r')
fname = '{}-{}-{}-q{}-{}'.format(
self.event.slug.upper(),
i.orderposition.order.code,

View File

@@ -5,7 +5,6 @@ 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
@@ -32,19 +31,17 @@ 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"),
help_text=_('Only used for views that are not bound to an event. For all '
'event views, the event timezone is used instead.')
)
# timezone = forms.ChoiceField(
# choices=((a, a) for a in common_timezones),
# label=_("Default timezone"),
# )
class Meta:
model = User
fields = [
'fullname',
'locale',
'timezone',
# 'timezone',
'email'
]

View File

@@ -331,12 +331,6 @@ 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))

View File

@@ -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,13 +186,11 @@ 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']))
@@ -220,14 +218,7 @@ class SecurityMiddleware(MiddlewareMixin):
domain = '%s:%d' % (domain, siteurlsplit.port)
dynamicdomain += " " + domain
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
if request.path not in self.CSP_EXEMPT:
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

View File

@@ -1,173 +0,0 @@
# -*- 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),
),
]

View File

@@ -1,40 +0,0 @@
# -*- 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'),
),
]

View File

@@ -1,32 +0,0 @@
# -*- 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'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- 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)'),
),
]

View File

@@ -1,28 +0,0 @@
# -*- 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
)
]

View File

@@ -1,25 +0,0 @@
# -*- 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),
),
]

View File

@@ -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, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
Event, Event_SettingsStore, EventLock, RequiredAction, SubEvent,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (

View File

@@ -251,24 +251,6 @@ 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()

View File

@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
class LoggingMixin:
def log_action(self, action, data=None, user=None, api_token=None):
def log_action(self, action, data=None, user=None):
"""
Create a LogEntry object that is related to this object.
See the LogEntry documentation for details.
@@ -53,12 +53,10 @@ class LoggingMixin:
event = self
elif hasattr(self, 'event'):
event = self.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)
l = LogEntry(content_object=self, user=user, action_type=action, event=event)
if data:
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
logentry.save()
l.data = json.dumps(data, cls=CustomJSONEncoder)
l.save()
class LoggedModel(models.Model, LoggingMixin):

View File

@@ -38,31 +38,6 @@ 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
@@ -194,7 +169,7 @@ class Event(EventMixin, LoggedModel):
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
verbose_name=_("Event name"),
verbose_name=_("Name"),
)
slug = models.SlugField(
max_length=50, db_index=True,
@@ -264,7 +239,7 @@ class Event(EventMixin, LoggedModel):
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
self.cache.clear()
self.get_cache().clear()
return obj
def get_plugins(self) -> "list[str]":
@@ -281,19 +256,6 @@ 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
@@ -591,16 +553,6 @@ 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)
@@ -667,7 +619,7 @@ class EventMetaProperty(LoggedModel):
name = models.CharField(
max_length=50, db_index=True,
help_text=_(
"Can not contain spaces or special characters except underscores"
"Can not contain spaces or special characters execpt underscores"
),
validators=[
RegexValidator(
@@ -700,16 +652,6 @@ 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):
"""
@@ -730,13 +672,3 @@ 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()

View File

@@ -81,7 +81,6 @@ 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):

View File

@@ -66,12 +66,12 @@ class ItemCategory(LoggedModel):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
@property
def sortkey(self):
@@ -104,16 +104,6 @@ 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):
"""
@@ -131,16 +121,6 @@ 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):
"""
@@ -195,7 +175,6 @@ 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,
@@ -310,12 +289,12 @@ class Item(LoggedModel):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
def tax(self, price=None, base_price_is='auto'):
price = price if price is not None else self.default_price
@@ -438,12 +417,12 @@ class ItemVariation(models.Model):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.item:
self.item.event.cache.clear()
self.item.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.item:
self.item.event.cache.clear()
self.item.event.get_cache().clear()
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
"""
@@ -615,12 +594,12 @@ class Question(LoggedModel):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
@property
def sortkey(self):
@@ -725,10 +704,6 @@ 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")
@@ -740,27 +715,14 @@ class Quota(LoggedModel):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
def save(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', True)
super().save(*args, **kwargs)
if self.event and clear_cache:
self.event.cache.clear()
if self.event:
self.event.get_cache().clear()
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]:
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
"""
This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale.
@@ -768,32 +730,12 @@ 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
@@ -805,9 +747,8 @@ class Quota(LoggedModel):
if size_left is None:
return Quota.AVAILABILITY_OK, None
paid_orders = self.count_paid_orders()
self.cached_availability_paid_orders = paid_orders
size_left -= paid_orders
# TODO: Test for interference with old versions of Item-Quota-relations, etc.
size_left -= self.count_paid_orders()
if size_left <= 0:
return Quota.AVAILABILITY_GONE, 0
@@ -867,7 +808,7 @@ class Quota(LoggedModel):
& Q(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gte=now_dt))
) &
self._position_lookup
).count()
).values('id').distinct().count()
def count_pending_orders(self) -> dict:
from pretix.base.models import Order, OrderPosition
@@ -875,23 +816,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
).count()
).values('id').distinct().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
).count()
).values('id').distinct().count()
@cached_property
def _position_lookup(self) -> Q:
return (
( # Orders for items which do not have any variations
Q(variation__isnull=True) &
Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.pk).values_list('item_id', flat=True))
Q(item__quotas=self)
) | ( # Orders for items which do have any variations
Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.pk).values_list('itemvariation_id', flat=True))
Q(variation__quotas=self)
)
)

View File

@@ -8,8 +8,6 @@ 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):
"""
@@ -35,7 +33,6 @@ 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='{}')
@@ -149,9 +146,6 @@ 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

View File

@@ -314,7 +314,7 @@ class Order(LoggedModel):
), tz)
return term_last
def _can_be_paid(self, count_waitinglist=True) -> Union[bool, str]:
def _can_be_paid(self) -> 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(count_waitinglist=count_waitinglist)
return self._is_still_available()
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
def _is_still_available(self, now_dt: datetime=None) -> 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, count_waitinglist=count_waitinglist)[1]
quota.cached_availability = quota.availability(now_dt)[1]
else:
# Use cached version
quota = quota_cache[quota.id]
@@ -728,6 +728,10 @@ 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:
@@ -828,7 +832,7 @@ class CartPosition(AbstractPosition):
verbose_name=_("Event")
)
cart_id = models.CharField(
max_length=255, null=True, blank=True, db_index=True,
max_length=255, null=True, blank=True,
verbose_name=_("Cart ID (e.g. session key)")
)
datetime = models.DateTimeField(
@@ -881,11 +885,6 @@ 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:

View File

@@ -3,7 +3,6 @@ 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
@@ -63,19 +62,6 @@ 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

View File

@@ -81,16 +81,6 @@ 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(
@@ -182,13 +172,3 @@ 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()

View File

@@ -4,7 +4,6 @@ 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 _
@@ -181,149 +180,33 @@ class Voucher(LoggedModel):
def __str__(self):
return self.code
def allow_delete(self):
return self.redeemed == 0
def clean(self):
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:
super().clean()
if self.quota:
if self.item:
raise ValidationError(_('You cannot select a quota and a specific product at the same time.'))
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):
elif self.item:
if self.variation and (not self.item or not self.item.has_variations):
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
'variations.'))
if variation and not item.variations.filter(pk=variation.pk).exists():
if self.variation and not self.item.variations.filter(pk=self.variation.pk).exists():
raise ValidationError(_('This variation does not belong to this product.'))
if item.has_variations and not variation and data.get('block_quota'):
if self.item.has_variations and not self.variation and self.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.'))
@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'):
if self.event.has_subevents and self.block_quota and not self.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.cache.set('vouchers_exist', True)
self.event.get_cache().set('vouchers_exist', True)
def delete(self, using=None, keep_parents=False):
super().delete(using, keep_parents)
self.event.cache.delete('vouchers_exist')
self.event.get_cache().delete('vouchers_exist')
def is_in_cart(self) -> bool:
"""

View File

@@ -21,7 +21,6 @@ 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__)
@@ -50,17 +49,6 @@ 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:
"""
@@ -161,9 +149,7 @@ class BasePaymentProvider:
('_fee_percent',
forms.DecimalField(
label=_('Additional fee'),
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.'),
help_text=_('Percentage'),
required=False
)),
('_availability_date',
@@ -187,7 +173,6 @@ 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'}}
)),
])
@@ -288,7 +273,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=get_or_create_cart_id(request))
return self._is_still_available(cart_id=request.session.session_key)
def payment_form_render(self, request: HttpRequest) -> str:
"""
@@ -606,11 +591,7 @@ class FreeOrderProvider(BasePaymentProvider):
messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool:
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
return get_cart_total(request) == 0
def order_change_allowed(self, order: Order) -> bool:
return False

View File

@@ -6,7 +6,6 @@ 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 = (
@@ -108,9 +107,6 @@ 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'
@@ -172,8 +168,6 @@ 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
)
@@ -283,34 +277,3 @@ 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)

View File

@@ -6,7 +6,6 @@ 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 _
@@ -20,11 +19,7 @@ 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):
@@ -61,8 +56,6 @@ 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.'),
@@ -320,7 +313,7 @@ class CartManager:
self._check_item_constraints(op)
operations.append(op)
self._quota_diff.update(quota_diff)
self._quota_diff += quota_diff
self._voucher_use_diff += voucher_use_diff
self._operations += operations
@@ -458,7 +451,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(v.quotas)
quotas = list(cp.quotas)
for quota in quotas:
quota_diff[quota] -= 1
@@ -466,14 +459,12 @@ class CartManager:
op = self.RemoveOperation(position=v)
operations.append(op)
self._quota_diff.update(quota_diff)
self._quota_diff += quota_diff
self._operations += operations
def _get_quota_availability(self):
quotas_ok = defaultdict(int)
quotas_ok = {}
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])
@@ -545,9 +536,6 @@ 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):
@@ -639,10 +627,13 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
return totaldiff
def get_fees(event, request, total, invoice_address, provider):
def get_fees(event, total, invoice_address, provider):
fees = []
if provider and total != 0:
if total == 0:
return fees
if provider:
provider = event.get_payment_providers().get(provider)
if provider:
payment_fee = provider.calculate_fee(total)
@@ -652,7 +643,7 @@ def get_fees(event, request, 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=OrderFee.FEE_TYPE_PAYMENT,
fee_type="PAYMENT",
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
@@ -660,16 +651,13 @@ def get_fees(event, request, total, invoice_address, provider):
))
else:
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
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
@@ -772,13 +760,3 @@ 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))
}

View File

@@ -54,14 +54,13 @@ def build_invoice(invoice: Invoice) -> Invoice:
{i.zipcode} {i.city}
{country}""")
invoice.invoice_to = addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old
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
).strip()
invoice.internal_reference = ia.internal_reference
if ia.vat_id:
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % ia.vat_id
if invoice.order.invoice_address.vat_id:
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % invoice.order.invoice_address.vat_id
cc = str(ia.country)
cc = str(invoice.order.invoice_address.country)
if cc in EU_CURRENCIES and EU_CURRENCIES[cc] != invoice.event.currency:
invoice.foreign_currency_display = EU_CURRENCIES[cc]

View File

@@ -82,7 +82,6 @@ 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)

View File

@@ -1,4 +1,3 @@
import copy
import json
import logging
from collections import Counter, namedtuple
@@ -24,11 +23,7 @@ from pretix.base.models import (
User, Voucher,
)
from pretix.base.models.event import SubEvent
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.orders import CachedTicket, InvoiceAddress, OrderFee
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider
from pretix.base.reldate import RelativeDateWrapper
@@ -39,10 +34,7 @@ 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 (
allow_ticket_download, order_fee_calculation, order_paid, order_placed,
periodic_task,
)
from pretix.base.signals import order_paid, order_placed, periodic_task
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -53,7 +45,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 occurred, please try again."),
'internal': _("An internal error occured, 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."),
@@ -80,8 +72,7 @@ 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='',
count_waitinglist=True, api_token=None) -> Order:
force: bool=False, send_mail: bool=True, user: User=None, mail_text='') -> Order:
"""
Marks an order as paid. This sets the payment provider, info and date and returns
the order object.
@@ -107,7 +98,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(count_waitinglist=count_waitinglist)
can_be_paid = order._can_be_paid()
if not force and can_be_paid is not True:
raise Quota.QuotaExceededException(can_be_paid)
order.payment_provider = provider or order.payment_provider
@@ -124,7 +115,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, api_token=api_token)
}, user=user)
order_paid.send(order.event, order=order)
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
@@ -163,46 +154,6 @@ 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):
"""
@@ -227,7 +178,7 @@ def mark_order_refunded(order, user=None):
@transaction.atomic
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
def _cancel_order(order, user=None, send_mail: bool=True):
"""
Mark this order as canceled
:param order: The order to change
@@ -237,15 +188,13 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
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, api_token=api_token)
order.log_action('pretix.event.order.canceled', user=user)
i = order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
@@ -393,8 +342,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
raise OrderError(err, errargs)
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider, address: InvoiceAddress,
meta_info: dict, event: Event):
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider):
fees = []
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
@@ -402,18 +350,15 @@ 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: InvoiceAddress=None,
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
meta_info: dict=None):
from datetime import time
fees = _get_fees(positions, payment_provider, address, meta_info, event)
fees = _get_fees(positions, payment_provider)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
tz = pytz.timezone(event.settings.timezone)
@@ -592,11 +537,7 @@ def send_expiry_warnings(sender, **kwargs):
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
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}
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
try:
o.send_mail(
email_subject, email_template, email_context,
@@ -620,9 +561,6 @@ 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
@@ -665,16 +603,13 @@ 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, notify=True):
def __init__(self, order: Order, user):
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]):
@@ -792,12 +727,6 @@ 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:
@@ -817,26 +746,12 @@ class OrderChangeManager:
def _check_paid_to_free(self):
if self.order.total == 0:
try:
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
)
mark_order_paid(self.order, 'free', send_mail=False)
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={
@@ -921,89 +836,23 @@ class OrderChangeManager:
'positionid': pos.positionid,
'subevent': op.subevent.pk if op.subevent else None,
})
elif isinstance(op, self.SplitOperation):
split_positions.append(op.position)
if split_positions:
self.split_order = self._create_split_order(split_positions)
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()
if payment_fee != 0:
fee.save()
elif fee.pk:
fee.delete()
split_order.total += fee.value
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()])
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 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()
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.total = sum([p.price for p in self.order.positions.all()]) + sum([f.value for f in self.order.fees.all()])
self.order.save()
def _reissue_invoice(self):
@@ -1013,9 +862,8 @@ class OrderChangeManager:
generate_invoice(self.order)
def _check_complete_cancel(self):
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:
cancels = len([o for o in self._operations if isinstance(o, self.CancelOperation)])
if cancels == self.order.positions.count():
raise OrderError(self.error_messages['complete_cancel'])
@property
@@ -1025,27 +873,27 @@ class OrderChangeManager:
except InvoiceAddress.DoesNotExist:
return None
def _notify_user(self, order):
with language(order.locale):
def _notify_user(self):
with language(self.order.locale):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = order.event.settings.mail_text_order_changed
email_template = self.order.event.settings.mail_text_order_changed
email_context = {
'event': order.event.name,
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
'order': self.order.code,
'secret': self.order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}
email_subject = _('Your order has been changed: %(code)s') % {'code': self.order.code}
try:
order.send_mail(
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', self.user
)
@@ -1069,10 +917,7 @@ class OrderChangeManager:
self._reissue_invoice()
self._clear_tickets_cache()
self._check_paid_to_free()
if self.notify:
self._notify_user(self.order)
if self.split_order:
self._notify_user(self.split_order)
self._notify_user()
def _clear_tickets_cache(self):
CachedTicket.objects.filter(order_position__order=self.order).delete()
@@ -1098,10 +943,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, api_token=None):
def cancel_order(self, order: int, user: int=None, send_mail: bool=True):
try:
try:
return _cancel_order(order, user, send_mail, api_token)
return _cancel_order(order, user, send_mail)
except LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -1,32 +0,0 @@
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()

View File

@@ -7,7 +7,6 @@ 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:
@@ -200,15 +199,9 @@ 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[pprov[0]], provider_names.get(pprov[1], pprov[1]))
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], provider_names.get(pprov[1], pprov[1]))
else:
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.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], pprov[1])
ppobj.provider = pprov[1]
ppobj.has_variations = False
ppobj.num_total = total

View File

@@ -28,34 +28,33 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
sent = 0
with event.lock():
for wle in qs:
if (wle.item, wle.variation) in gone:
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
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
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))
# 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

View File

@@ -209,10 +209,6 @@ DEFAULTS = {
'default': None,
'type': str
},
'confirm_text': {
'default': None,
'type': LazyI18nString
},
'mail_prefix': {
'default': None,
'type': str
@@ -427,14 +423,6 @@ 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

View File

@@ -138,9 +138,7 @@ order_placed = EventPluginSignal(
)
"""
This signal is sent out every time an order is placed. The order object is given
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 the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -150,8 +148,7 @@ order_paid = EventPluginSignal(
)
"""
This signal is sent out every time an order is paid. The order object is given
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 the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -169,34 +166,6 @@ 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"]
)
@@ -240,37 +209,3 @@ 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.
"""

View File

@@ -4,7 +4,6 @@ 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
@@ -57,19 +56,13 @@ ALLOWED_ATTRIBUTES = {
def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/')
if not is_safe_url(url) and not url.startswith('mailto:'):
if not is_safe_url(url):
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):
"""
@@ -80,5 +73,5 @@ def rich_text(text: str, **kwargs):
markdown.markdown(text),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
), callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]))
), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
return mark_safe(body_md)

View File

@@ -4,17 +4,7 @@ register = template.Library()
@register.simple_tag
def url_replace(request, *pairs):
def url_replace(request, field, value):
dict_ = request.GET.copy()
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='[]')
dict_[field] = value
return dict_.urlencode()

View File

@@ -32,8 +32,6 @@ class EventSlugBlacklistValidator(BlacklistValidator):
'__debug__',
'api',
'events',
'csp_report',
'widget',
]
@@ -53,6 +51,4 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
'__debug__',
'about',
'api',
'csp_report',
'widget',
]

View File

@@ -51,9 +51,6 @@ 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:
@@ -62,11 +59,10 @@ class AsyncAction:
pass
ready = res.ready()
data = self._ajax_response_data()
data.update({
data = {
'async_id': res.id,
'ready': ready
})
}
if ready:
if res.successful() and not isinstance(res.info, Exception):
smes = self.get_success_message(res.info)
@@ -133,7 +129,7 @@ class AsyncAction:
return str(exception)
else:
logger.error('Unexpected exception: %r' % exception)
return _('An unexpected error has occurred.')
return _('An unexpected error has occured.')
def get_success_message(self, value):
return _('The task has been completed.')

View File

@@ -1,24 +0,0 @@
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()

View File

@@ -29,14 +29,14 @@ def contextprocessor(request):
'DEBUG': settings.DEBUG,
}
_html_head = []
if hasattr(request, 'event') and request.user.is_authenticated:
if hasattr(request, 'event'):
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') and request.user.is_authenticated:
if getattr(request, 'event', None) and hasattr(request, 'organizer'):
for receiver, response in nav_event.send(request.event, request=request):
_nav_event += response
if request.event.settings.get('payment_term_weekdays'):
@@ -61,16 +61,15 @@ def contextprocessor(request):
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
_nav_global = []
if not hasattr(request, 'event') and request.user.is_authenticated:
if not hasattr(request, 'event'):
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 = []
if request.user.is_authenticated:
for receiver, response in nav_topbar.send(request, request=request):
_nav_topbar += response
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')

View File

@@ -1,9 +1,7 @@
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
@@ -100,34 +98,3 @@ 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)

View File

@@ -4,17 +4,15 @@ 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 pgettext_lazy, ugettext_lazy as _
from django.utils.translation import 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, SubEvent
from pretix.base.models.event import EventMetaValue
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import (
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
)
from pretix.control.forms import ExtFileField, SlugWidget
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.style import get_fonts
@@ -48,8 +46,6 @@ 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):
@@ -58,7 +54,7 @@ class EventWizardBasicsForm(I18nModelForm):
}
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Event timezone"),
label=_("Default timezone"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
@@ -68,7 +64,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 "
"detailed configuration later."),
"detailled configuration later."),
required=False
)
@@ -84,18 +80,14 @@ 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': 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,
'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
}
def __init__(self, *args, **kwargs):
@@ -107,9 +99,6 @@ 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']
@@ -199,9 +188,6 @@ 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
@@ -218,19 +204,14 @@ 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': 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'}),
'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'}),
}
@@ -265,7 +246,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 tickets for "
"answers to questions. If you use the event series feature and an order contains tickest for "
"multiple event dates, the earliest date will be used."),
required=False,
)
@@ -358,14 +339,6 @@ 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,
@@ -393,14 +366,6 @@ 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(
@@ -552,51 +517,25 @@ class InvoiceSettingsForm(SettingsForm):
choices=[]
)
invoice_address_from = forms.CharField(
widget=forms.Textarea(attrs={
'rows': 5,
'placeholder': _(
'Sample Event Company\n'
'Albert Einstein Road 52\n'
'12345 Samplecity'
)
}),
required=False,
widget=forms.Textarea(attrs={'rows': 5}), 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.")
"your jurisdiction (e.g. your VAT ID).")
)
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.")
@@ -639,13 +578,7 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextarea,
help_text=_("This will be attached to every email. Available placeholders: {event}"),
validators=[PlaceholderValidator(['{event}'])],
widget_kwargs={'attrs': {
'rows': '4',
'placeholder': _(
'e.g. your contact details'
)
}}
validators=[PlaceholderValidator(['{event}'])]
)
mail_text_order_placed = I18nFormField(
@@ -750,17 +683,14 @@ class MailSettingsForm(SettingsForm):
)
smtp_host = forms.CharField(
label=_("Hostname"),
required=False,
widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'})
required=False
)
smtp_port = forms.IntegerField(
label=_("Port"),
required=False,
widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'})
required=False
)
smtp_username = forms.CharField(
label=_("Username"),
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
required=False
)
smtp_password = forms.CharField(
@@ -897,44 +827,3 @@ 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

View File

@@ -1,47 +1,12 @@
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 Event, Invoice, Item, Order, Organizer, SubEvent
from pretix.base.models import 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):
@@ -71,13 +36,6 @@ class OrderFilterForm(FilterForm):
}),
required=False
)
provider = forms.ChoiceField(
label=_('Payment provider'),
choices=[
('', _('All payment providers')),
],
required=False,
)
status = forms.ChoiceField(
label=_('Order status'),
choices=(
@@ -135,25 +93,23 @@ 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(),
@@ -161,6 +117,17 @@ 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)
@@ -183,14 +150,13 @@ 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(),
@@ -207,7 +173,6 @@ 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
@@ -222,8 +187,7 @@ class OrderSearchFilterForm(OrderFilterForm):
class SubEventFilterForm(FilterForm):
orders = {
'date_from': 'date_from',
'active': 'active',
'sum_quota_available': 'sum_quota_available'
'active': 'active'
}
status = forms.ChoiceField(
label=_('Status'),
@@ -284,8 +248,7 @@ class EventFilterForm(FilterForm):
'organizer': 'organizer__name',
'date_from': 'order_from',
'date_to': 'order_to',
'live': 'live',
'sum_tickets_paid': 'sum_tickets_paid'
'live': 'live'
}
status = forms.ChoiceField(
label=_('Status'),

View File

@@ -12,7 +12,6 @@ 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):
@@ -48,9 +47,7 @@ class QuestionForm(I18nModelForm):
'items'
]
widgets = {
'items': forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
'items': forms.CheckboxSelectMultiple,
}
@@ -152,18 +149,14 @@ 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=choices,
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"))
),
initial=self.NONE,
required=False
)
@@ -185,7 +178,7 @@ class ItemCreateForm(I18nModelForm):
self.fields['quota_add_new_size'] = forms.IntegerField(
min_value=0,
label=_("Size"),
widget=forms.TextInput(attrs={'placeholder': _("Number of tickets")}),
widget=forms.TextInput(attrs={'placeholder': _("New quota size")}),
help_text=_("Leave empty for an unlimited number of tickets."),
required=False
)
@@ -270,11 +263,6 @@ 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
@@ -298,13 +286,9 @@ class ItemUpdateForm(I18nModelForm):
'min_per_order',
'checkin_attention'
]
field_classes = {
'available_from': forms.SplitDateTimeField,
'available_until': forms.SplitDateTimeField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
'available_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'available_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
}

View File

@@ -15,13 +15,6 @@ 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']
@@ -32,11 +25,6 @@ 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)
@@ -90,14 +78,6 @@ 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')
@@ -187,8 +167,7 @@ class OrderPositionChangeForm(forms.Form):
('product', 'Change product'),
('price', 'Change price'),
('subevent', 'Change event date'),
('cancel', 'Remove product'),
('split', 'Split into new order'),
('cancel', 'Remove product')
)
)

View File

@@ -1,7 +1,6 @@
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
@@ -9,7 +8,6 @@ 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):
@@ -68,9 +66,7 @@ class OrganizerUpdateForm(OrganizerForm):
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif current_domain:
current_domain.delete()
instance.cache.clear()
for ev in instance.events.all():
ev.cache.clear()
instance.get_cache().clear()
return instance
@@ -117,6 +113,20 @@ 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,
@@ -124,23 +134,6 @@ 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"),
@@ -148,6 +141,7 @@ class OrganizerDisplaySettingsForm(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=(
@@ -155,22 +149,3 @@ class OrganizerDisplaySettingsForm(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()
]

View File

@@ -1,54 +0,0 @@
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

View File

@@ -5,7 +5,6 @@ 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):
@@ -28,19 +27,13 @@ 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': 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'}),
'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'}),
}

View File

@@ -2,11 +2,12 @@ import copy
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, 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
@@ -26,11 +27,8 @@ 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': SplitDateTimePickerWidget(),
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
}
def __init__(self, *args, **kwargs):
@@ -90,41 +88,113 @@ 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']
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)
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_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)
@@ -148,11 +218,8 @@ 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': SplitDateTimePickerWidget(),
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
}
labels = {
'max_usages': _('Maximum usages per voucher')

View File

@@ -1,11 +1,8 @@
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
@@ -85,28 +82,13 @@ 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 changed.'),
'pretix.event.order.modified': _('The order details have been modified.'),
'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.'),
@@ -126,12 +108,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 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.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.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.'),
@@ -143,27 +125,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 changed.'),
'pretix.voucher.changed': _('The voucher has been modified.'),
'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 changed.'),
'pretix.event.item.changed': _('The product has been modified.'),
'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 changed.'),
'pretix.event.item.variation.changed': _('The variation "{value}" has been modified.'),
'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 changed.'),
'pretix.event.quota.changed': _('The quota has been modified.'),
'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 changed.'),
'pretix.event.category.changed': _('The category has been modified.'),
'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 changed.'),
'pretix.event.question.changed': _('The question has been modified.'),
'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.'),
@@ -183,13 +165,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 changed.'),
'pretix.team.changed': _('The team settings have been modified.'),
'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 changed.'),
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been modified.'),
'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 changed on the event date.'),
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been modified on the event date.'),
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
}
@@ -218,21 +200,6 @@ 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'))

View File

@@ -181,43 +181,3 @@ 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.
"""

View File

@@ -52,7 +52,6 @@
<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>
@@ -75,11 +74,6 @@
{% 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>
@@ -113,10 +107,6 @@
</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 %}

View File

@@ -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 "Event dashboard" %}
{% trans "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 is_event_settings or "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
{% if "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
<i class="fa fa-wrench fa-fw"></i>
{% trans "Settings" %}
</a>

View File

@@ -7,6 +7,6 @@
<p>{{ text }}</p>
{% endif %}
{% if button_text %}
<p><a href="{{ button_url }}" class="btn btn-primary btn-lg">{{ button_text }}</a></p>
<p><a href="{{ button_url }}" class="btn btn-default btn-lg">{{ button_text }}</a></p>
{% endif %}
</div>

View File

@@ -1,24 +1,17 @@
{% 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 "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 %}
<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" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -7,21 +7,21 @@
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Invoicing" %}</legend>
{% 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" %}
{% 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" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">

View File

@@ -8,9 +8,9 @@
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "E-mail settings" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_prefix layout="horizontal" %}
{% bootstrap_field form.mail_from layout="horizontal" %}
{% bootstrap_field form.mail_text_signature layout="horizontal" %}
</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="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" %}
{% 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" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -6,12 +6,12 @@
{% csrf_token %}
<fieldset>
<legend>{% trans "Payment settings" %}</legend>
{% 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" %}
{% 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" %}
</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='control' %}
{% bootstrap_form provider.form layout='horizontal' %}
{% with c=provider.settings_content %}
{% if c %}{{ c|safe }}{% endif %}
{% endwith %}

View File

@@ -7,14 +7,14 @@
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% 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" %}
{% 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" %}
{% if meta_forms %}
<div class="form-group metadata-group">
@@ -38,41 +38,40 @@
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% 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" %}
{% 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" %}
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% 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" %}
{% 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" %}
</fieldset>
<fieldset>
<legend>{% trans "Orders" %}</legend>
{% 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" %}
{% 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" %}
</fieldset>
<fieldset>
<legend>{% trans "Waiting list" %}</legend>
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours layout="control" %}
{% bootstrap_field sform.waiting_list_enabled layout="horizontal" %}
{% bootstrap_field sform.waiting_list_auto layout="horizontal" %}
{% bootstrap_field sform.waiting_list_hours layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -3,30 +3,6 @@
{% 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 %}
@@ -75,19 +51,7 @@
{% 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 %}

View File

@@ -17,8 +17,8 @@
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.rate addon_after="%" layout="control" %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.rate layout="horizontal" %}
<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="control" %}
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
{% bootstrap_field form.price_includes_tax layout="horizontal" %}
{% bootstrap_field form.eu_reverse_charge layout="horizontal" %}
{% bootstrap_field form.home_country layout="horizontal" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -15,10 +15,10 @@
</div>
{% endif %}
{% bootstrap_form_errors form %}
{% 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" %}
{% 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" %}
{% for provider in providers %}
<div class="panel panel-default ticketoutput-panel">
<div class="panel-heading">

View File

@@ -1,71 +0,0 @@
{% 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>&lt;head&gt;</code>
section of your website:
{% endblocktrans %}
</p>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}"&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async&gt;&lt;/script&gt;</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>&lt;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 %}&gt;&lt;/pretix-widget&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
{% 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 &lt;a {{ a_attr }}&gt;click here&lt;/a&gt;.
{% endblocktrans %}
&lt;/div&gt;
&lt;/div&gt;
&lt;/noscript&gt;
</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 %}

View File

@@ -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 %}

View File

@@ -4,7 +4,7 @@
{% block form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.name layout="horizontal" %}
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.slug.id_for_label }}">{{ form.slug.label }}</label>
<div class="col-md-9 form-inline">
@@ -29,22 +29,22 @@
</div>
</div>
</div>
{% 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.currency layout="control" %}
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% bootstrap_field form.locale layout="control" %}
{% bootstrap_field form.timezone layout="control" %}
{% bootstrap_field form.locale layout="horizontal" %}
{% bootstrap_field form.timezone layout="horizontal" %}
</fieldset>
{% if form.presale_start %}
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.presale_end layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %}
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
</fieldset>
{% endif %}
{% endblock %}

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