Compare commits

...

68 Commits

Author SHA1 Message Date
Raphael Michel
91c02dc0b3 Bump version to 2.1.0 2018-10-04 11:33:09 +02:00
Raphael Michel
f78ec830b5 Fix pretix-stripe.js 2018-10-03 17:31:06 +02:00
Raphael Michel
9f0e508ab3 Do not require meta_noindex 2018-10-03 12:52:37 +02:00
Raphael Michel
4ca50d750b Merge pull request #1037 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-10-03 12:44:14 +02:00
Raphael Michel
07c1b1b7f3 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2773 of 2773 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
.. _`rest-oauth`:
OAuth support / "Connect with pretix"
=====================================
OAuth authentication / "Connect with pretix"
============================================
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
@@ -168,4 +168,4 @@ pretix user interface.
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication

View File

@@ -41,6 +41,10 @@ plugins list A list of packa
The ``plugins`` field has been added.
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 2.1
Filters have been added to the list of events.
Endpoints
---------
@@ -96,6 +100,12 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only events with a matching value of ``is_public`` are returned.
:query live: If set to ``true``/``false``, only events with a matching value of ``live`` are returned.
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure

View File

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

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

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

View File

@@ -96,8 +96,6 @@ The provider class
.. automethod:: order_change_allowed
.. automethod:: order_can_retry
.. automethod:: payment_prepare
.. automethod:: payment_control_render

View File

@@ -23,6 +23,7 @@ cronjob
cryptographic
debian
deduplication
deprovision
discoverable
django
dockerfile

View File

@@ -1 +1 @@
__version__ = "2.1.0.dev0"
__version__ = "2.1.0"

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django_countries.serializers import CountryFieldMixin
from rest_framework.fields import Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event, TaxRule
@@ -190,12 +191,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
event = SlugRelatedField(slug_field='slug', read_only=True)
meta_data = MetaDataField(source='*')
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location',
'presale_start', 'presale_end', 'location', 'event',
'item_price_overrides', 'variation_price_overrides', 'meta_data')

View File

@@ -77,7 +77,8 @@ class CheckinSerializer(I18nAwareModelSerializer):
class OrderDownloadsField(serializers.Field):
def to_representation(self, instance: Order):
if instance.status != Order.STATUS_PAID:
return []
if instance.status != Order.STATUS_PENDING or instance.require_approval or not instance.event.settings.ticket_download_pending:
return []
request = self.context['request']
res = []
@@ -100,7 +101,8 @@ class OrderDownloadsField(serializers.Field):
class PositionDownloadsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
if instance.order.status != Order.STATUS_PAID:
return []
if instance.order.status != Order.STATUS_PENDING or instance.order.require_approval or not instance.order.event.settings.ticket_download_pending:
return []
if instance.addon_to_id and not instance.order.event.settings.ticket_download_addons:
return []
if not instance.item.admission and not instance.order.event.settings.ticket_download_nonadm:
@@ -129,12 +131,19 @@ class PdfDataSerializer(serializers.Field):
res = {}
ev = instance.subevent or instance.order.event
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list.
pdfvars = get_variables(instance.order.event)
for k, f in pdfvars.items():
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
for k, v in ev.meta_data.items():
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
return res
@@ -507,6 +516,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if any(errs):
raise ValidationError({'positions': errs})
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filterset_class = ItemFilter
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -92,7 +92,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -154,7 +154,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -210,7 +210,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = ItemCategoryFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -264,7 +264,8 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = QuestionFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
@@ -307,7 +308,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):
@@ -362,7 +363,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
filterset_class = QuotaFilter
ordering_fields = ('id', 'size')
ordering = ('id',)
permission = 'can_change_items'
permission = None
write_permission = 'can_change_items'
def get_queryset(self):

View File

@@ -3,7 +3,7 @@ import datetime
import django_filters
import pytz
from django.db import transaction
from django.db.models import Q
from django.db.models import Prefetch, Q
from django.db.models.functions import Concat
from django.http import FileResponse
from django.shortcuts import get_object_or_404
@@ -25,7 +25,7 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
Device, Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken,
)
from pretix.base.payment import PaymentException
@@ -72,13 +72,34 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return ctx
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'positions__answers__question', 'fees', 'payments', 'refunds', 'refunds__payment'
qs = self.request.event.orders.prefetch_related(
'fees', 'payments', 'refunds', 'refunds__payment'
).select_related(
'invoice_address'
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
)
)
)
else:
qs = qs.prefetch_related(
Prefetch(
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
@@ -177,6 +198,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
device=request.auth if isinstance(request.auth, Device) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail
)
@@ -191,7 +213,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
approve_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
)
except Quota.QuotaExceededException as e:
@@ -210,7 +232,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
deny_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
comment=comment,
)
@@ -267,7 +289,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_order_refunded(
order,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
)
return self.retrieve(request, [], **kwargs)

View File

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

View File

@@ -57,7 +57,7 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
kwargs['locales'] = self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
for f in self.fields.values():
for k, f in self.fields.items():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)

View File

@@ -16,6 +16,7 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -143,7 +144,7 @@ class BaseQuestionsForm(forms.Form):
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
field = forms.SplitDateTimeField(
field = SplitDateTimeField(
label=q.question, required=q.required,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,

View File

@@ -2,6 +2,7 @@ import os
from django import forms
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
@@ -92,14 +93,20 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
def date_placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
def time_placeholder():
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
date_attrs['placeholder'] = lazy(date_placeholder, str)
time_attrs['placeholder'] = lazy(time_placeholder, str)
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),

View File

@@ -379,7 +379,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Your reference: {reference}').format(reference=self.invoice.internal_reference),
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
self.stylesheet['Normal']
))

View File

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

View File

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

View File

@@ -47,6 +47,7 @@ class LoggingMixin:
"""
from .log import LogEntry
from .event import Event
from .devices import Device
from pretix.api.models import OAuthAccessToken, OAuthApplication
from .organizer import TeamAPIToken
from ..notifications import get_all_notification_types
@@ -67,6 +68,8 @@ class LoggingMixin:
kwargs['oauth_application'] = auth
elif isinstance(auth, TeamAPIToken):
kwargs['api_token'] = auth
elif isinstance(auth, Device):
kwargs['device'] = auth
elif isinstance(api_token, TeamAPIToken):
kwargs['api_token'] = api_token
@@ -96,4 +99,4 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token')
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')

View File

@@ -0,0 +1,155 @@
import string
from django.db import models
from django.db.models import Max
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import LoggedModel
def generate_serial():
serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16)
while Device.objects.filter(unique_serial=serial).exists():
serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16)
return serial
def generate_initialization_token():
token = get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
while Device.objects.filter(initialization_token=token).exists():
token = get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
return token
def generate_api_token():
token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
while Device.objects.filter(api_token=token).exists():
token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
return token
class Device(LoggedModel):
organizer = models.ForeignKey(
'pretixbase.Organizer',
on_delete=models.PROTECT,
related_name='devices'
)
device_id = models.PositiveIntegerField()
unique_serial = models.CharField(max_length=190, default=generate_serial, unique=True)
initialization_token = models.CharField(max_length=190, default=generate_initialization_token, unique=True)
api_token = models.CharField(max_length=190, unique=True, null=True)
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
name = models.CharField(
max_length=190,
verbose_name=_('Name')
)
created = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Setup date')
)
initialized = models.DateTimeField(
verbose_name=_('Initialization date'),
null=True,
)
hardware_brand = models.CharField(
max_length=190,
null=True, blank=True
)
hardware_model = models.CharField(
max_length=190,
null=True, blank=True
)
software_brand = models.CharField(
max_length=190,
null=True, blank=True
)
software_version = models.CharField(
max_length=190,
null=True, blank=True
)
class Meta:
unique_together = (('organizer', 'device_id'),)
def __str__(self):
return '#{}: {} ({} {})'.format(
self.device_id, self.name, self.hardware_brand, self.hardware_model
)
def save(self, *args, **kwargs):
if not self.device_id:
self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1
super().save(*args, **kwargs)
def permission_set(self) -> set:
return {
'can_view_orders',
'can_change_orders',
}
def get_event_permission_set(self, organizer, event) -> set:
"""
Gets a set of permissions (as strings) that a token holds for a particular event
:param organizer: The organizer of the event
:param event: The event to check
:return: set of permissions
"""
has_event_access = (self.all_events and organizer == self.organizer) or (
event in self.limit_events.all()
)
return self.permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
Gets a set of permissions (as strings) that a token holds for a particular organizer
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.all_events and organizer == self.organizer) or (
event in self.limit_events.all()
)
if isinstance(perm_name, (tuple, list)):
return has_event_access and any(p in self.permission_set() for p in perm_name)
return has_event_access and (not perm_name or perm_name in self.permission_set())
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
if isinstance(perm_name, (tuple, list)):
return organizer == self.organizer and any(p in self.permission_set() for p in perm_name)
return organizer == self.organizer and (not perm_name or perm_name in self.permission_set())
def get_events_with_any_permission(self):
"""
Returns a queryset of events the token has any permissions to.
:return: Iterable of Events
"""
if self.all_events:
return self.organizer.events.all()
else:
return self.limit_events.all()

View File

@@ -276,12 +276,24 @@ class Event(EventMixin, LoggedModel):
else:
return super().presale_has_ended
def delete_all_orders(self, really=False):
from .orders import OrderRefund, OrderPayment, OrderPosition, OrderFee
if not really:
raise TypeError("Pass really=True as a parameter.")
OrderPosition.objects.all().delete(order__event=self)
OrderFee.objects.all().delete(order__event=self)
OrderPayment.objects.all().delete(order__event=self)
OrderRefund.objects.all().delete(order__event=self)
self.orders.all().delete()
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
self.cache.clear()
return obj
def get_plugins(self) -> "list[str]":
def get_plugins(self):
"""
Returns the names of the plugins activated for this event as a list.
"""

View File

@@ -41,6 +41,7 @@ class LogEntry(models.Model):
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)
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
action_type = models.CharField(max_length=255)

View File

@@ -420,6 +420,20 @@ class Order(LoggedModel):
dl_date = dl_date.datetime(self.event)
return dl_date
@property
def ticket_download_available(self):
return self.event.settings.ticket_download and (
self.event.settings.ticket_download_date is None
or now() > self.ticket_download_date
) and (
self.status == Order.STATUS_PAID
or (
(self.event.settings.ticket_download_pending or self.total == Decimal("0.00")) and
self.status == Order.STATUS_PENDING and
not self.require_approval
)
)
@property
def payment_term_last(self):
tz = pytz.timezone(self.event.settings.timezone)
@@ -496,7 +510,7 @@ class Order(LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None):
auth=None, attach_tickets=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -512,6 +526,7 @@ class Order(LoggedModel):
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
@@ -525,7 +540,7 @@ class Order(LoggedModel):
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
@@ -882,6 +897,22 @@ class OrderPayment(models.Model):
"""
return self.order.event.get_payment_providers().get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
if not force and can_be_paid is not True:
raise Quota.QuotaExceededException(can_be_paid)
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.log_action('pretix.event.order.paid', {
'provider': self.provider,
'info': self.info,
'date': self.payment_date,
'force': force
}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
@@ -901,7 +932,6 @@ class OrderPayment(models.Model):
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.signals import order_paid
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -928,20 +958,14 @@ class OrderPayment(models.Model):
if payment_sum - refund_sum < self.order.total:
return
with self.order.event.lock():
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
if not force and can_be_paid is not True:
raise Quota.QuotaExceededException(can_be_paid)
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.log_action('pretix.event.order.paid', {
'provider': self.provider,
'info': self.info,
'date': self.payment_date,
'force': force
}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
if self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12):
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
with transaction.atomic():
self._mark_paid(force, count_waitinglist, user, auth)
else:
with self.order.event.lock():
self._mark_paid(force, count_waitinglist, user, auth)
invoice = None
if invoice_qualified(self.order):
@@ -982,7 +1006,8 @@ class OrderPayment(models.Model):
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else []
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')

View File

@@ -58,7 +58,7 @@ class Organizer(LoggedModel):
self.get_cache().clear()
return obj
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
def get_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

View File

@@ -14,12 +14,14 @@ from django.http import HttpRequest
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, Order, OrderPayment, OrderRefund, Quota,
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -28,7 +30,7 @@ from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import get_or_create_cart_id
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
@@ -179,7 +181,7 @@ class BasePaymentProvider:
implementation.
"""
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
return OrderedDict([
d = OrderedDict([
('_enabled',
forms.BooleanField(
label=_('Enable payment method'),
@@ -250,7 +252,20 @@ class BasePaymentProvider:
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
('_restricted_countries',
forms.MultipleChoiceField(
label=_('Restrict to countries'),
choices=Countries(),
help_text=_('Only allow choosing this payment provider for invoice addresses in the selected '
'countries. If you don\'t select any country, all countries are allowed.'),
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
required=False
)),
])
d['_restricted_countries']._as_type = list
return d
def settings_content_render(self, request: HttpRequest) -> str:
"""
@@ -350,7 +365,8 @@ class BasePaymentProvider:
during checkout, not on retrying.
The default implementation checks for the _availability_date setting to be either unset or in the future
and for the _total_max and _total_min requirements to be met.
and for the _total_max and _total_min requirements to be met. It also checks the ``_restrict_countries``
setting.
:param total: The total value without the payment method fee, after taxes.
@@ -371,6 +387,25 @@ class BasePaymentProvider:
if self.settings._total_min is not None:
pricing = pricing and total >= Decimal(self.settings._total_min)
def get_invoice_address():
if not hasattr(request, '_checkout_flow_invoice_address'):
cs = cart_session(request)
iapk = cs.get('invoice_address')
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
restricted_countries = self.settings.get('_restricted_countries', as_type=list)
if restricted_countries:
ia = get_invoice_address()
if str(ia.country) not in restricted_countries:
return False
return timing and pricing
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
@@ -503,7 +538,8 @@ class BasePaymentProvider:
Will be called to check whether it is allowed to change the payment method of
an order to this one.
The default implementation checks for the _availability_date setting to be either unset or in the future.
The default implementation checks for the _availability_date setting to be either unset or in the future,
as well as for the _total_max, _total_min and _restricted_countries settings.
:param order: The order object
"""
@@ -514,6 +550,16 @@ class BasePaymentProvider:
if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
return False
restricted_countries = self.settings.get('_restricted_countries', as_type=list)
if restricted_countries:
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
return True
else:
if str(ia.country) not in restricted_countries:
return False
return self._is_still_available(order=order)
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:

View File

@@ -161,7 +161,10 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Addon 1\nAddon 2"),
"evaluate": lambda op, order, ev: "<br/>".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.select_related('item', 'variation')
for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
])
}),
("organizer", {

View File

@@ -200,10 +200,10 @@ def regenerate_invoice(invoice: Invoice):
def generate_invoice(order: Order, trigger_pdf=True):
locale = order.event.settings.get('invoice_language')
locale = order.event.settings.get('invoice_language', order.event.settings.locale)
if locale:
if locale == '__user__':
locale = order.locale
locale = order.locale or order.event.settings.locale
invoice = Invoice(
order=order,

View File

@@ -14,6 +14,7 @@ from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -35,7 +36,8 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None):
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None,
attach_tickets=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -65,6 +67,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param invoices: A list of invoices to attach to this email.
:param attach_tickets: Whether to attach tickets to this email, if they are available to download.
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -153,7 +157,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
event=event.id if event else None,
headers=headers,
invoices=[i.pk for i in invoices] if invoices else [],
order=order.pk if order else None
order=order.pk if order else None,
attach_tickets=attach_tickets
)
if invoices:
@@ -168,7 +173,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task
def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None) -> bool:
order: int=None, attach_tickets=False) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(html, "text/html")
@@ -185,6 +190,7 @@ def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sen
except:
logger.exception('Could not attach invoice to email')
pass
if event:
event = Event.objects.get(id=event)
backend = event.get_mail_backend()
@@ -197,6 +203,15 @@ def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sen
order = event.orders.get(pk=order)
except Order.DoesNotExist:
order = None
else:
if attach_tickets:
for name, ct in get_tickets_for_order(order):
email.attach(
name,
ct.file.read(),
ct.type
)
email = email_filter.send_chained(event, 'message', message=email, order=order)
try:

View File

@@ -21,7 +21,7 @@ from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
)
from pretix.base.models import (
CartPosition, Event, Item, ItemVariation, Order, OrderPayment,
CartPosition, Device, Event, Item, ItemVariation, Order, OrderPayment,
OrderPosition, Quota, User, Voucher,
)
from pretix.base.models.event import SubEvent
@@ -307,7 +307,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
@transaction.atomic
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_application=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -319,6 +319,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_
user = User.objects.get(pk=user)
if isinstance(api_token, int):
api_token = TeamAPIToken.objects.get(pk=api_token)
if isinstance(device, int):
device = Device.objects.get(pk=device)
if isinstance(oauth_application, int):
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
with order.event.lock():
@@ -327,7 +329,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_
order.status = Order.STATUS_CANCELED
order.save()
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device)
i = order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
@@ -631,7 +633,8 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
@@ -733,7 +736,8 @@ def send_download_reminders(sender, **kwargs):
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent'
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
except SendMailException:
logger.exception('Reminder email could not be sent')
@@ -1299,10 +1303,11 @@ 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, oauth_application=None):
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None):
try:
try:
return _cancel_order(order, user, send_mail, api_token, oauth_application)
return _cancel_order(order, user, send_mail, api_token, device, oauth_application)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -1,3 +1,4 @@
import logging
import os
from datetime import timedelta
@@ -11,10 +12,12 @@ from pretix.base.models import (
OrderPosition,
)
from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import register_ticket_outputs
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction
logger = logging.getLogger(__name__)
@app.task(base=ProfiledTask)
def generate(order_position: str, provider: str):
@@ -97,7 +100,8 @@ def preview(event: int, provider: str):
return prov.generate(p)
def get_cachedticket_for_position(pos, identifier):
def get_cachedticket_for_position(pos, identifier, generate_async=True):
apply_method = 'apply_async' if generate_async else 'apply'
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=identifier
@@ -109,15 +113,20 @@ def get_cachedticket_for_position(pos, identifier):
ct = CachedTicket.objects.create(
order_position=pos, provider=identifier,
extension='', type='', file=None)
generate.apply_async(args=(pos.id, identifier))
getattr(generate, apply_method)(args=(pos.id, identifier))
if not generate_async:
ct.refresh_from_db()
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate.apply_async(args=(pos.id, identifier))
getattr(generate, apply_method)(args=(pos.id, identifier))
if not generate_async:
ct.refresh_from_db()
return ct
def get_cachedticket_for_order(order, identifier):
def get_cachedticket_for_order(order, identifier, generate_async=True):
apply_method = 'apply_async' if generate_async else 'apply'
try:
ct = CachedCombinedTicket.objects.filter(
order=order, provider=identifier
@@ -129,9 +138,63 @@ def get_cachedticket_for_order(order, identifier):
ct = CachedCombinedTicket.objects.create(
order=order, provider=identifier,
extension='', type='', file=None)
generate_order.apply_async(args=(order.id, identifier))
getattr(generate_order, apply_method)(args=(order.id, identifier))
if not generate_async:
ct.refresh_from_db()
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate_order.apply_async(args=(order.id, identifier))
getattr(generate_order, apply_method)(args=(order.id, identifier))
if not generate_async:
ct.refresh_from_db()
return ct
def get_tickets_for_order(order):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
return []
if not order.ticket_download_available:
return []
providers = [
response(order.event)
for receiver, response
in register_ticket_outputs.send(order.event)
]
tickets = []
for p in providers:
if not p.is_enabled:
continue
if p.multi_download_enabled:
try:
ct = get_cachedticket_for_order(order, p.identifier, generate_async=False)
tickets.append((
"{}-{}-{}{}".format(
order.event.slug.upper(), order.code, ct.provider, ct.extension,
),
ct
))
except:
logger.exception('Failed to generate ticket.')
else:
for pos in order.positions.all():
if pos.addon_to and not order.event.settings.ticket_download_addons:
continue
if not pos.item.admission and not order.event.settings.ticket_download_nonadm:
continue
try:
ct = get_cachedticket_for_position(pos, p.identifier, generate_async=False)
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
),
ct
))
except:
logger.exception('Failed to generate ticket.')
return tickets

View File

@@ -16,6 +16,6 @@
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</body>
</html>

View File

@@ -1,7 +1,10 @@
import datetime
import os
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.utils import from_current_timezone
from django.utils.html import conditional_escape
from django.utils.translation import ugettext_lazy as _
@@ -168,3 +171,17 @@ class SingleLanguageWidget(forms.Select):
def optgroups(self, name, value, attrs=None):
self.modify()
return super().optgroups(name, value, attrs)
class SplitDateTimeField(forms.SplitDateTimeField):
def compress(self, data_list):
# Differs from the default implementation: If only a time is given and no date, we consider the field empty
if data_list:
if data_list[0] in self.empty_values:
return None
if data_list[1] in self.empty_values:
raise ValidationError(self.error_messages['invalid_date'], code='invalid_date')
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None

View File

@@ -22,7 +22,7 @@ from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import (
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimePickerWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
@@ -95,10 +95,10 @@ class EventWizardBasicsForm(I18nModelForm):
'location',
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
'date_from': SplitDateTimeField,
'date_to': SplitDateTimeField,
'presale_start': SplitDateTimeField,
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(),
@@ -229,11 +229,11 @@ class EventUpdateForm(I18nModelForm):
'location',
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'date_admission': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
'date_from': SplitDateTimeField,
'date_to': SplitDateTimeField,
'date_admission': SplitDateTimeField,
'presale_start': SplitDateTimeField,
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(),
@@ -311,7 +311,7 @@ class EventSettingsForm(SettingsForm):
help_text=_("If a ticket voucher is sent to a person on the waiting list, it has to be redeemed within this "
"number of hours until it expires and can be re-assigned to the next person on the list."),
required=False,
widget=forms.NumberInput(attrs={'data-display-dependency': '#id_settings-waiting_list_enabled'}),
widget=forms.NumberInput(),
)
waiting_list_auto = forms.BooleanField(
label=_("Automatic waiting list assignments"),
@@ -319,7 +319,7 @@ class EventSettingsForm(SettingsForm):
"on the waiting list for that product. If this is not active, mails will not be send automatically "
"but you can send them manually via the control panel."),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-waiting_list_enabled'}),
widget=forms.CheckboxInput(),
)
attendee_names_asked = forms.BooleanField(
label=_("Ask for attendee names"),
@@ -497,6 +497,9 @@ class ProviderForm(SettingsForm):
elif isinstance(v, (RelativeDateTimeField, RelativeDateField)):
v.set_event(self.obj)
if hasattr(v, '_as_type'):
self.initial[k] = self.obj.settings.get(k, as_type=v._as_type)
def clean(self):
cleaned_data = super().clean()
enabled = cleaned_data.get(self.settingspref + '_enabled')
@@ -916,6 +919,10 @@ class DisplaySettingsForm(SettingsForm):
('name_descending', _('Name (descending)')),
], # When adding a new ordering, remember to also define it in the event model
)
meta_noindex = forms.BooleanField(
label=_('Ask search engines not to index the ticket shop'),
required=False
)
def __init__(self, *args, **kwargs):
event = kwargs['obj']
@@ -943,12 +950,14 @@ class TicketSettingsForm(SettingsForm):
ticket_download_addons = forms.BooleanField(
label=_("Offer to download tickets separately for add-on products"),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
)
ticket_download_nonadm = forms.BooleanField(
label=_("Generate tickets for non-admission products"),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
)
ticket_download_pending = forms.BooleanField(
label=_("Offer to download tickets even before an order is paid"),
required=False,
)
def prepare_fields(self):

View File

@@ -14,7 +14,7 @@ from pretix.base.models import (
)
from pretix.base.models.items import ItemAddOn
from pretix.base.signals import item_copy_data
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2
from pretix.helpers.models import modelcopy
from pretix.helpers.money import change_decimal_field
@@ -330,8 +330,8 @@ class ItemUpdateForm(I18nModelForm):
'original_price'
]
field_classes = {
'available_from': forms.SplitDateTimeField,
'available_until': forms.SplitDateTimeField,
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),

View File

@@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Organizer, Team
from pretix.base.models import Device, Organizer, Team
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
@@ -107,7 +107,7 @@ class TeamForm(forms.ModelForm):
data = super().clean()
if self.instance.pk and not data['can_change_teams']:
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
can_change_teams=True, members__isnull=False
can_change_teams=True, members__isnull=False
).exists():
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
'the permission to change teams and permissions.'))
@@ -115,6 +115,23 @@ class TeamForm(forms.ModelForm):
return data
class DeviceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields['limit_events'].queryset = organizer.events.all()
class Meta:
model = Device
fields = ['name', 'all_events', 'limit_events']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
}),
}
class OrganizerSettingsForm(SettingsForm):
organizer_info_text = I18nFormField(

View File

@@ -12,7 +12,7 @@ from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.helpers.money import change_decimal_field
@@ -37,11 +37,11 @@ class SubEventForm(I18nModelForm):
'frontpage_text'
]
field_classes = {
'date_from': forms.SplitDateTimeField,
'date_to': forms.SplitDateTimeField,
'date_admission': forms.SplitDateTimeField,
'presale_start': forms.SplitDateTimeField,
'presale_end': forms.SplitDateTimeField,
'date_from': SplitDateTimeField,
'date_to': SplitDateTimeField,
'date_admission': SplitDateTimeField,
'presale_start': SplitDateTimeField,
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(),

View File

@@ -6,7 +6,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import voucher_form_validation
from pretix.helpers.models import modelcopy
@@ -34,7 +34,7 @@ class VoucherForm(I18nModelForm):
'comment', 'max_usages', 'price_mode', 'subevent'
]
field_classes = {
'valid_until': forms.SplitDateTimeField,
'valid_until': SplitDateTimeField,
}
widgets = {
'valid_until': SplitDateTimePickerWidget(),
@@ -190,7 +190,7 @@ class VoucherBulkForm(VoucherForm):
'max_usages', 'price_mode', 'subevent'
]
field_classes = {
'valid_until': forms.SplitDateTimeField,
'valid_until': SplitDateTimeField,
}
widgets = {
'valid_until': SplitDateTimePickerWidget(),

View File

@@ -198,6 +198,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'),
'pretix.event.order.refund.created': _('Refund {local_id} has been created.'),
'pretix.event.order.refund.created.externally': _('Refund {local_id} has been created by an external entity.'),
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
@@ -274,6 +275,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'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.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
'pretix.device.created': _('The device has been created.'),
'pretix.device.changed': _('The device has been changed.'),
'pretix.device.revoked': _('Access of the device has been revoked.'),
'pretix.device.initialized': _('The device has been initialized.'),
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
}
data = json.loads(logentry.data)

View File

@@ -11,6 +11,7 @@
{% bootstrap_field form.logo_image layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.show_variations_expanded layout="control" %}
{% bootstrap_field form.meta_noindex layout="control" %}
{% if form.frontpage_subevent_ordering %}
{% bootstrap_field form.frontpage_subevent_ordering layout="control" %}
{% endif %}

View File

@@ -167,6 +167,9 @@
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.device %}
<span class="fa fa-mobile fa-fw"></span>
{{ log.device.name }}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
@@ -179,6 +182,12 @@
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
{% if staff_session %}
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
{% endif %}
</div>
</div>
</li>

View File

@@ -31,7 +31,8 @@
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-12">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
<span class="fa fa-clock-o"></span>
{{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if log.shredded %}
<span class="fa fa-eraser fa-danger fa-fw"
data-toggle="tooltip"
@@ -54,6 +55,9 @@
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.device %}
<span class="fa fa-mobile fa-fw"></span>
{{ log.device.name }}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
@@ -66,6 +70,12 @@
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
{% if staff_session %}
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
{% endif %}
</div>
</div>
</li>

View File

@@ -19,6 +19,7 @@
{% 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_pending layout="control" %}
{% for provider in providers %}
<div class="panel panel-default ticketoutput-panel">
<div class="panel-heading">

View File

@@ -19,6 +19,9 @@
<span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.device %}
<span class="fa fa-mobile fa-fw"></span>
{{ log.device.name }}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
@@ -27,13 +30,18 @@
<span class="fa fa-eraser fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "Personal data was cleared from this log entry." %}">
</span>
{% endif %}
</p>
<p>
{{ log.display }}
{% if staff_session %}
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
{% endif %}
</p>
</li>
{% endfor %}

View File

@@ -33,6 +33,13 @@
</a>
</li>
{% endif %}
{% if 'can_change_organizer_settings' in request.orgapermset %}
<li {% if "organizer.device" in url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.devices" organizer=organizer.slug %}">
{% trans "Devices" %}
</a>
</li>
{% endif %}
{% for nav in nav_organizer %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">

View File

@@ -0,0 +1,38 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load bootstrap3 %}
{% block inner %}
<legend>{% trans "Connect to device:" %} {{ device.name }}</legend>
<div>
<ol>
<li>{% trans "Open the app that you want to connect and optionally reset it to the original state." %}</li>
<li>{% trans "Scan the following configuration code:" %}<br><br>
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
<br>
<strong>{% trans "System URL:" %}</strong> {{ settings.SITE_URL }}<br>
<strong>{% trans "Token:" %}</strong> {{ device.initialization_token }}
</li>
</ol>
</div>
<div class="alert alert-warning">
<strong>
{% blocktrans trimmed %}
Please note that this is a new feature that currently only works for beta-stage software, such as
pretixPOS. pretixdroid 1.x and pretixdesk 0.x are not supported by this feature. Future versions of
pretixdroid and pretixdesk will be supported through this menu.
{% endblocktrans %}
</strong>
<br><br>
{% blocktrans trimmed %}
To set up pretixdroid or pretixdesk, please go to the <strong>Check-in devices</strong> section of an event.
{% endblocktrans %}
</div>
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-arrow-left"></i>
{% trans "Device overview" %}
</a>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/devices.js" %}"></script>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
{% if device %}
<legend>{% trans "Device:" %} {{ device.name }}</legend>
{% else %}
<legend>{% trans "Connect a new device" %}</legend>
{% endif %}
<form class="form-horizontal" action="" method="post">
{% if device %}
<div class="row">
<div class="col-xs-12 col-lg-10">
{% endif %}
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.all_events layout="control" %}
{% bootstrap_field form.limit_events layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% if device %}
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Device history" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=device %}
</div>
</div>
</div>
{% endif %}
</form>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>{% trans "Revoke device access:" %} {{ device.name }}</h2>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>
<strong>{% blocktrans %}Are you sure you want remove access for this device?{% endblocktrans %}</strong>
{% trans "All data of this device will stay available, but you can't use the device any more." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug%}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Revoke" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,116 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<legend>
{% trans "Connected devices" %}
</legend>
<div class="alert alert-info">
{% blocktrans trimmed %}
This menu allows you to connect hardware devices such as box office terminals or scanning terminals to
your account.
{% endblocktrans %}
<strong>
{% blocktrans trimmed %}
Please note that this is a new feature that currently only works for beta-stage software, such as
pretixPOS. pretixdroid 1.x and pretixdesk 0.x are not supported by this feature. Future versions of
pretixdroid and pretixdesk will be supported through this menu.
{% endblocktrans %}
</strong>
<br><br>
{% blocktrans trimmed %}
To set up pretixdroid or pretixdesk, please go to the <strong>Check-in devices</strong> section of an event.
{% endblocktrans %}
</div>
{% if devices|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't connected any hardware devices yet.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
</div>
{% else %}
<p>
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
</p>
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Device ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Hardware model" %}</th>
<th>{% trans "Software" %}</th>
<th>{% trans "Setup date" %}</th>
<th>{% trans "Events" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for d in devices %}
<tr>
<td>
{{ d.device_id }}
</td>
<td>
{% if d.initialized and not d.api_token %}<del>{% endif %}
{{ d.name }}
{% if d.initialized and not d.api_token %}</del>{% endif %}
</td>
<td>
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
</td>
<td>
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
</td>
<td>
{% if d.initialized %}
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
<em>{% trans "Not yet initialized" %}</em>
{% endif %}
{% if d.initialized and not d.api_token %}
<span class="label label-danger">{% trans "Revoked" %}</span>
{% endif %}
</td>
<td>
{% if d.all_events %}
{% trans "All" %}
{% else %}
<ul>
{% for e in d.limit_events.all %}
<li>
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
{{ e }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right">
{% if not d.initialized %}
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
{% trans "Connect" %}</a>
{% elif d.api_token %}
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
{% trans "Revoke access" %}</a>
{% endif %}
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete vouchers" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete vouchers" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if allowed %}
<p>{% blocktrans %}Are you sure you want to delete the following vouchers?{% endblocktrans %}</p>
<ul>
{% for s in allowed %}
<li>
{{ s }}
<input type="hidden" name="voucher" value="{{ s.pk }}">
</li>
{% endfor %}
</ul>
{% endif %}
{% if forbidden %}
<p>{% blocktrans trimmed %}The following vouchers can't be deleted as they already have been redeemed, but they will be set to fully redeemed instead.{% endblocktrans %}</p>
<ul>
{% for s in forbidden %}
<li>
{{ s }}
<input type="hidden" name="voucher" value="{{ s.pk }}">
</li>
{% endfor %}
</ul>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save" value="delete_confirm" name="action">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -83,56 +83,73 @@
class="btn btn-default"><i class="fa fa-download"></i>
{% trans "Download list" %}</a>
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Voucher code" %}</th>
<th>{% trans "Redemptions" %}</th>
<th>{% trans "Expiry" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Product" %}</th>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th></th>
</tr>
</thead>
<tbody>
{% for v in vouchers %}
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<td>
<strong><a href="
{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
</td>
<td>{{ v.redeemed }} / {{ v.max_usages }}</td>
<td>{{ v.valid_until|date }}</td>
<td>
{{ v.tag }}
</td>
<td>
{% if v.item %}
{{ v.item }}
{% if v.variation %}
{{ v.variation }}
{% endif %}
{% else %}
{% blocktrans trimmed with quota=v.quota.name %}
Any product in quota "{{ quota }}"
{% endblocktrans %}
<th>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" data-toggle-table />
{% endif %}
</td>
</th>
<th>{% trans "Voucher code" %}</th>
<th>{% trans "Redemptions" %}</th>
<th>{% trans "Expiry" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Product" %}</th>
{% if request.event.has_subevents %}
<td>{{ v.subevent.name }} {{ v.subevent.get_date_range_display }}</td>
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<td class="text-right">
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
<th></th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</thead>
<tbody>
{% for v in vouchers %}
<tr>
<td>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" name="voucher" class="" value="{{ v.pk }}"/>
{% endif %}
</td>
<td>
<strong><a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
</td>
<td>{{ v.redeemed }} / {{ v.max_usages }}</td>
<td>{{ v.valid_until|date }}</td>
<td>
{{ v.tag }}
</td>
<td>
{% if v.item %}
{{ v.item }}
{% if v.variation %}
{{ v.variation }}
{% endif %}
{% else %}
{% blocktrans trimmed with quota=v.quota.name %}
Any product in quota "{{ quota }}"
{% endblocktrans %}
{% endif %}
</td>
{% if request.event.has_subevents %}
<td>{{ v.subevent.name }} {{ v.subevent.get_date_range_display }}</td>
{% endif %}
<td class="text-right">
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if "can_change_vouchers" in request.eventpermset %}
<button type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %}
</button>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -18,6 +18,7 @@ urlpatterns = [
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
url(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
url(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^sudo/$', user.StartStaffSession.as_view(), name='user.sudo'),
url(r'^sudo/stop/$', user.StopStaffSession.as_view(), name='user.sudo.stop'),
@@ -68,6 +69,15 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
name='organizer.display'),
url(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
url(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
name='organizer.device.add'),
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/edit$', organizer.DeviceUpdateView.as_view(),
name='organizer.device.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/connect$', organizer.DeviceConnectView.as_view(),
name='organizer.device.connect'),
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/revoke$', organizer.DeviceRevokeView.as_view(),
name='organizer.device.revoke'),
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
url(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
@@ -169,6 +179,7 @@ urlpatterns = [
url(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'),
url(r'^vouchers/go$', vouchers.VoucherGo.as_view(), name='event.vouchers.go'),
url(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
url(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
url(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
name='event.order.transition'),
url(r'^orders/(?P<code>[0-9A-Z]+)/resend$', orders.OrderResendLink.as_view(),

View File

@@ -142,21 +142,22 @@ def invite(request, token):
if request.method == 'POST':
form = RegistrationForm(data=request.POST)
if form.is_valid():
user = User.objects.create_user(
form.cleaned_data['email'], form.cleaned_data['password'],
locale=request.LANGUAGE_CODE,
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
)
user = authenticate(request=request, email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
request.session['pretix_auth_long_session'] = (
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
)
with transaction.atomic():
valid = form.is_valid()
if valid:
user = User.objects.create_user(
form.cleaned_data['email'], form.cleaned_data['password'],
locale=request.LANGUAGE_CODE,
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
)
user = authenticate(request=request, email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
request.session['pretix_auth_long_session'] = (
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
)
with transaction.atomic():
inv.team.members.add(request.user)
inv.team.log_action(
'pretix.team.member.joined', data={

View File

@@ -254,7 +254,8 @@ def event_index(request, organizer, event):
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request)
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application').order_by('-datetime')
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application',
'device').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders', request=request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))

View File

@@ -875,7 +875,7 @@ class EventLog(EventPermissionRequiredMixin, ListView):
def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related(
'user', 'content_type', 'api_token', 'oauth_application'
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
@@ -1197,9 +1197,21 @@ class QuickSetupView(FormView):
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': 'pretix.plugins.ticketoutputpdf'})
plugins_active.append('pretix.plugins.ticketoutputpdf')
self.request.event.settings.ticket_download = True
self.request.event.settings.ticketoutput_pdf__enabled = True
try:
import pretix_passbook # noqa
except ImportError:
pass
else:
if 'pretix_passbook' not in plugins_active:
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': 'pretix_passbook'})
plugins_active.append('pretix_passbook')
self.request.event.settings.ticketoutput_passbook__enabled = True
if form.cleaned_data['payment_banktransfer__enabled']:
if 'pretix.plugins.banktransfer' not in plugins_active:
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,

View File

@@ -1,8 +1,11 @@
from django.contrib import messages
from django.shortcuts import redirect, reverse
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.generic import FormView, TemplateView
from pretix.base.models import LogEntry
from pretix.base.services.update_check import check_result_table, update_check
from pretix.base.settings import GlobalSettingsObject
from pretix.control.forms.global_settings import (
@@ -62,3 +65,9 @@ class UpdateCheckView(StaffMemberRequiredMixin, FormView):
class MessageView(TemplateView):
template_name = 'pretixcontrol/global_message.html'
class LogDetailView(AdministratorPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
le = get_object_or_404(LogEntry, pk=request.GET.get('pk'))
return JsonResponse({'data': le.parsed_data})

View File

@@ -866,7 +866,8 @@ class OrderResendLink(OrderView):
email_subject = _('Your order: %(code)s') % {'code': self.order.code}
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=self.request.user
'pretix.event.order.email.resend', user=self.request.user,
attach_tickets=True
)
except SendMailException:
messages.error(self.request, _('There was an error sending the mail. Please try again later.'))

View File

@@ -1,10 +1,14 @@
import json
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
from django.db.models import Count
from django.forms import inlineformset_factory
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
@@ -13,14 +17,14 @@ from django.views.generic import (
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
)
from pretix.base.models import Organizer, Team, TeamInvite, User
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
from pretix.base.models.event import EventMetaProperty
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import OrganizerFilterForm
from pretix.control.forms.organizer import (
EventMetaPropertyForm, OrganizerDisplaySettingsForm, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
DeviceForm, EventMetaPropertyForm, OrganizerDisplaySettingsForm,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
)
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer
@@ -576,3 +580,139 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
'organizer': self.request.organizer.slug,
'team': self.object.pk
})
class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Device
template_name = 'pretixcontrol/organizers/devices.html'
permission = 'can_change_organizer_settings'
context_object_name = 'devices'
def get_queryset(self):
return self.request.organizer.devices.prefetch_related('limit_events')
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
form_class = DeviceForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['organizer'] = self.request.organizer
return kwargs
def get_success_url(self):
return reverse('control:organizer.device.connect', kwargs={
'organizer': self.request.organizer.slug,
'device': self.object.pk
})
def form_valid(self, form):
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.log_action('pretix.device.created', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
return ret
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
context_object_name = 'device'
form_class = DeviceForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['organizer'] = self.request.organizer
return kwargs
def get_object(self, queryset=None):
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
def get_success_url(self):
return reverse('control:organizer.devices', kwargs={
'organizer': self.request.organizer.slug,
})
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.device.changed', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_connect.html'
permission = 'can_change_organizer_settings'
context_object_name = 'device'
def get_object(self, queryset=None):
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if 'ajax' in request.GET:
return JsonResponse({
'initialized': bool(self.object.initialized)
})
if self.object.initialized:
messages.success(request, _('This device has been set up successfully.'))
return redirect(reverse('control:organizer.devices', kwargs={
'organizer': self.request.organizer.slug,
}))
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['qrdata'] = json.dumps({
'handshake_version': 1,
'url': settings.SITE_URL,
'token': self.object.initialization_token,
})
return ctx
class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_revoke.html'
permission = 'can_change_organizer_settings'
context_object_name = 'device'
def get_object(self, queryset=None):
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.api_token:
messages.success(request, _('This device currently does not have access.'))
return redirect(reverse('control:organizer.devices', kwargs={
'organizer': self.request.organizer.slug,
}))
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.api_token = None
self.object.save()
self.object.log_action('pretix.device.revoked', user=self.request.user)
messages.success(request, _('Access for this device has been revoked.'))
return redirect(reverse('control:organizer.devices', kwargs={
'organizer': self.request.organizer.slug,
}))

View File

@@ -217,6 +217,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
if form in self.cl_formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.checkins.all().delete()
form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
form.instance.delete()
form.instance.pk = None

View File

@@ -9,7 +9,7 @@ from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import redirect
from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
@@ -322,3 +322,41 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class VoucherBulkAction(EventPermissionRequiredMixin, View):
permission = 'can_change_vouchers'
@cached_property
def objects(self):
return self.request.event.vouchers.filter(
id__in=self.request.POST.getlist('voucher')
)
@transaction.atomic
def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
'allowed': self.objects.filter(redeemed=0),
'forbidden': self.objects.exclude(redeemed=0),
})
elif request.POST.get('action') == 'delete_confirm':
for obj in self.objects:
if obj.allow_delete():
obj.log_action('pretix.voucher.deleted', user=self.request.user)
obj.delete()
else:
obj.log_action('pretix.voucher.changed', user=self.request.user, data={
'max_usages': min(obj.redeemed, obj.max_usages),
'bulk': True
})
obj.max_usages = min(obj.redeemed, obj.max_usages)
obj.save(update_fields=['max_usages'])
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})

View File

@@ -99,6 +99,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
except WaitingListEntry.DoesNotExist:
messages.error(request, _('Waiting list entry not found.'))
return self._redirect_back()
return self._redirect_back()
def _redirect_back(self):
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None):

View File

@@ -27,7 +27,11 @@ def get_sizes(size, imgsize):
if crop:
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if wfactor > hfactor:
if wfactor == hfactor:
return (int(imgsize[0] * wfactor), int(imgsize[1] * hfactor)), \
(0, int((imgsize[1] * wfactor - imgsize[1] * hfactor) / 2),
imgsize[0] * hfactor, int((imgsize[1] * wfactor + imgsize[1] * wfactor) / 2))
elif wfactor > hfactor:
return (int(size[0]), int(imgsize[1] * hfactor)), \
(0, int((imgsize[1] * wfactor - size[1]) / 2), size[0], int((imgsize[1] * wfactor + size[1]) / 2))
else:
@@ -36,7 +40,9 @@ def get_sizes(size, imgsize):
else:
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if wfactor < hfactor:
if wfactor == hfactor:
return (int(imgsize[0] * hfactor), int(imgsize[1] * wfactor)), None
elif wfactor < hfactor:
return (size[0], int(imgsize[1] * wfactor)), None
else:
return (int(imgsize[0] * hfactor), size[1]), None

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -184,7 +184,7 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -183,7 +183,7 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -195,7 +195,7 @@ msgstr "Alle"
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-08-11 08:17+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -199,7 +199,7 @@ msgstr "Alle"
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-08-11 08:20+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -199,7 +199,7 @@ msgstr "Alle"
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -184,7 +184,7 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-09-10 16:00+0000\n"
"Last-Translator: oocf <oswaldocerna@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -200,7 +200,7 @@ msgstr "Todos"
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-07-13 06:00+0000\n"
"Last-Translator: Claude <superoxyde@laposte.net>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -200,7 +200,7 @@ msgstr "Tous"
msgid "None"
msgstr "Aucun"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Utiliser un nom différent en interne"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -183,7 +183,7 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"PO-Revision-Date: 2018-08-20 20:00+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-09-16 19:47+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.0.1\n"
"X-Generator: Weblate 3.1.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -48,7 +48,7 @@ msgstr "Verbinding maken met Stripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:56
msgid "Total"
msgstr ""
msgstr "Totaal"
#: pretix/static/pretixbase/js/asyncdownload.js:28
#: pretix/static/pretixbase/js/asynctask.js:42
@@ -194,7 +194,7 @@ msgstr "Alle"
msgid "None"
msgstr "Geen"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Gebruik intern een andere naam"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -183,7 +183,7 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-06-04 19:48+0000\n"
"Last-Translator: wallber azevedo pinheiro <wallpinheiro@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
@@ -197,7 +197,7 @@ msgstr "Todos"
msgid "None"
msgstr "Nenhum"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-11 14:43+0000\n"
"POT-Creation-Date: 2018-10-03 10:25+0000\n"
"PO-Revision-Date: 2018-09-03 06:36+0000\n"
"Last-Translator: Yunus Fırat Pişkin <firat.piskin@idvlabs.com>\n"
"Language-Team: Turkish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -197,7 +197,7 @@ msgstr "Herşey"
msgid "None"
msgstr "Hiçbiri"
#: pretix/static/pretixcontrol/js/ui/main.js:531
#: pretix/static/pretixcontrol/js/ui/main.js:541
msgid "Use a different name internally"
msgstr "Dahili olarak farklı bir ad kullan"

View File

@@ -5,6 +5,7 @@ from rest_framework.exceptions import PermissionDenied
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from pretix.base.models import Device
from pretix.base.models.organizer import TeamAPIToken
from .models import BankImportJob, BankTransaction
@@ -68,7 +69,7 @@ class BankImportJobViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return serializer.save()
def create(self, request, *args, **kwargs):
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user)
if not perm_holder.has_organizer_permission(request.organizer, 'can_change_orders'):
raise PermissionDenied('Invalid set of permissions')
serializer = self.get_serializer(data=request.data)

View File

@@ -96,15 +96,11 @@ class BankTransfer(BasePaymentProvider):
}
return template.render(ctx)
def order_control_render(self, request, order) -> str:
if order.payment_info:
payment_info = json.loads(order.payment_info)
else:
payment_info = None
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
template = get_template('pretixplugins/banktransfer/control.html')
ctx = {'request': request, 'event': self.event,
'code': self._code(order),
'payment_info': payment_info, 'order': order}
'code': self._code(payment.order),
'payment_info': payment.info_data, 'order': payment.order}
return template.render(ctx)
def _code(self, order):

View File

@@ -245,6 +245,10 @@ class Paypal(BasePaymentProvider):
if payment.state != 'approved':
payment_obj.state = OrderPayment.PAYMENT_STATE_FAILED
payment_obj.save()
payment_obj.order.log_action('pretix.event.order.payment.failed', {
'local_id': payment.local_id,
'provider': payment.provider,
})
logger.error('Invalid state: %s' % str(payment))
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))

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