Compare commits

..

15 Commits

Author SHA1 Message Date
Raphael Michel
55d246f82e REmove totod 2018-07-16 10:34:55 +02:00
Raphael Michel
e9f0af1898 More views 2018-07-16 10:34:23 +02:00
Raphael Michel
053de88173 Navigational context selector 2018-07-14 16:39:19 +02:00
Raphael Michel
886b938f08 Allow plugins to add sub navigation points 2018-07-14 15:07:35 +02:00
Raphael Michel
59245c4ec3 New navi mechanism 2018-07-14 14:23:12 +02:00
Raphael Michel
71664e5203 Navigation behaviour 2018-07-09 15:40:06 +02:00
Raphael Michel
ce3ae5c218 CSS changes 2018-07-09 13:41:45 +02:00
Raphael Michel
9be5ec2417 Check-in and voucher templates 2018-07-09 08:56:01 +02:00
Raphael Michel
93b07a476d Minor css changes 2018-07-09 00:23:34 +02:00
Raphael Michel
d583775132 Order-related templates 2018-07-09 00:14:22 +02:00
Raphael Michel
0daded8af5 Item-related templates 2018-07-08 23:55:26 +02:00
Raphael Michel
7b230726b0 More pages 2018-07-08 22:31:28 +02:00
Raphael Michel
365d78f63c control stylesheet 2018-07-08 18:51:34 +02:00
Raphael Michel
50aa186197 Error pages 2018-07-08 18:15:14 +02:00
Raphael Michel
32f401e423 Login page 2018-07-08 17:44:13 +02:00
612 changed files with 40251 additions and 92737 deletions

View File

@@ -17,7 +17,7 @@ pypi:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz

View File

@@ -1 +1,2 @@
-r src/requirements/py34.txt
-r doc/requirements.txt

View File

@@ -11,16 +11,17 @@ fi
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
psql -c 'create database travis_ci_test;' -U postgres
pip3 install -Ur src/requirements/postgres.txt
fi
if [ "$1" == "style" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
cd src
flake8 .
isort -c -rc -df .
fi
if [ "$1" == "doctests" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt -r src/requirements/py34.txt
cd doc
make doctest
fi
@@ -38,21 +39,21 @@ if [ "$1" == "translation-spelling" ]; then
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt pytest-xdist
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt pytest-xdist
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 3 tests
py.test --reruns 5 -n 2 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
cd src
python manage.py check
make all compress
coverage run -m py.test --reruns 5 tests && codecov
fi
if [ "$1" == "plugins" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
cd src
python setup.py develop
make all compress

View File

@@ -1,7 +1,7 @@
language: python
sudo: false
install:
- pip install -U pip wheel setuptools
- pip install -U pip wheel setuptools==28.6.1
script:
- bash .travis.sh $JOB
cache:
@@ -18,8 +18,20 @@ matrix:
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
@@ -30,7 +42,6 @@ matrix:
env: JOB=translation-spelling
addons:
postgresql: "9.4"
mariadb: '10.3'
apt:
packages:
- enchant

View File

@@ -30,7 +30,7 @@ RUN chmod +x /usr/local/bin/pretix && \
pip3 install -U pip wheel setuptools && \
cd /pretix/src && \
rm -f pretix.cfg && \
pip3 install -r requirements.txt -r requirements/mysql.txt \
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \

View File

@@ -53,10 +53,6 @@ Example::
A comma-separated list of plugins that are enabled by default for all new events.
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
``plugins_exclude``
A comma-separated list of plugins that are not available even though they are installed.
Defaults to an empty string.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `MySQL`_ or `PostgreSQL`_ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -36,9 +36,6 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
On this guide
-------------
@@ -61,7 +58,7 @@ Next, we need a database and a database user. We can create these with any kind
our database's shell, e.g. for MySQL::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;

View File

@@ -21,9 +21,6 @@ To use pretix, you will need the following things:
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `MySQL`_ or `PostgreSQL`_ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -33,9 +33,6 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
Unix user
---------
@@ -53,7 +50,7 @@ Having the database server installed, we still need a database and a database us
of database managing tool or directly on our database's shell, e.g. for MySQL::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
@@ -124,7 +121,8 @@ command if you're running PostgreSQL::
(venv)$ pip3 install "pretix[mysql]" gunicorn
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
If you are running Python 3.4, you also need to ``pip3 install typing``. This is not required on 3.5 or newer.
You can find out your Python version using ``python -V``.
We also need to create a data directory::

View File

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

View File

@@ -1,137 +0,0 @@
.. _`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,20 +9,44 @@ with pretix' REST API, such as authentication, pagination and similar definition
Authentication
--------------
To access the API, you need to present valid authentication credentials. pretix currently
supports the following authorization schemes:
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).
* :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`_.
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
Permissions
-----------
@@ -148,7 +172,6 @@ Field specific input errors include the name of the offending fields as keys in
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
Data types
----------
@@ -181,4 +204,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,7 +14,5 @@ in functionality over time.
:maxdepth: 2
fundamentals
auth
oauth
resources/index
ratelimit
webhooks

View File

@@ -1,7 +1,7 @@
.. _`rest-oauth`:
OAuth authentication / "Connect with pretix"
============================================
OAuth support / "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
@@ -166,42 +166,6 @@ endpoint to revoke it.
If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
pretix user interface.
Fetching the user profile
-------------------------
If you need the user's meta data, you can fetch it here:
.. http:get:: /api/v1/me
Returns the profile of the authenticated user
**Example request**:
.. sourcecode:: http
GET /api/v1/me HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
email: "admin@localhost",
fullname: "John Doe",
locale: "de",
timezone: "Europe/Berlin"
}
:statuscode 200: no error
:statuscode 401: Authentication failure
.. _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

@@ -1,31 +0,0 @@
.. _`rest-ratelimit`:
Rate limiting
=============
.. note:: This page only applies to the pretix Hosted service at pretix.eu. APIs of custom pretix installations do not
enforce any rate limiting by default.
All authenticated requests to pretix' API are rate limited. If you exceed the limits, you will receive a response
with HTTP status code ``429 Too Many Requests``. This response will have a ``Retry-After`` header, containing the number
of seconds you are supposed to wait until you try again. We expect that all API clients respect this. If you continue
to burst requests after a ``429`` status code, we might get in touch with you or, in extreme cases, disable your API
access.
Currently, the following rate limits apply:
.. rst-class:: rest-resource-table
===================================== =================================================================================
Authentication method Rate limit
===================================== =================================================================================
:ref:`rest-deviceauth` 360 requests per minute per device
:ref:`rest-tokenauth` 360 requests per minute per organizer account
:ref:`rest-oauth` 360 requests per minute per combination of accessed organizer and OAuth application
Session authentication *Not an officially supported authentication method for external access*
===================================== =================================================================================
If you require a higher rate limit, please get in touch at support@pretix.eu and tell us about your use case, we are
sure we can work something out.

View File

@@ -25,7 +25,6 @@ item integer ID of the item
variation integer ID of the variation (or ``null``)
price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Composition of attendee name (i.e. first name, last name, …)
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
@@ -79,7 +78,6 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -124,7 +122,6 @@ Cart position endpoints
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_name_parts": {},
"attendee_email": null,
"voucher": null,
"addon_to": null,
@@ -178,7 +175,7 @@ Cart position endpoints
* ``item``
* ``variation`` (optional)
* ``price``
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_name`` (optional)
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
@@ -202,10 +199,7 @@ Cart position endpoints
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"given_name": "Peter",
"family_name": "Miller"
},
"attendee_name": "Peter",
"attendee_email": null,
"answers": [
{

View File

@@ -332,10 +332,6 @@ Order position endpoints
The ``.../redeem/`` endpoint has been added.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as
@@ -371,9 +367,6 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -429,8 +422,6 @@ Order position endpoints
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
:query integer addon_to__in: Only return positions that are add-ons to one of the positions with the given
comma-separated IDs.
:query string voucher: Only return positions with a specific voucher.
:query string voucher__code: Only return positions with a specific voucher code.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param list: The ID of the check-in list to look for
@@ -469,9 +460,6 @@ Order position endpoints
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",

View File

@@ -41,10 +41,6 @@ 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
---------
@@ -100,12 +96,6 @@ 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

@@ -21,4 +21,3 @@ Resources and endpoints
checkinlists
waitinglist
carts
webhooks

View File

@@ -59,9 +59,6 @@ checkin_attention boolean If ``True``, th
a product is being scanned.
original_price money (string) An original price, shown for comparison, not used
for price calculations.
require_approval boolean If ``True``, orders with this product will need to be
approved by the event organizer before they can be
paid.
has_variations boolean Shows whether or not this item has variations.
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
@@ -99,11 +96,7 @@ addons list of objects Definition of a
.. versionchanged:: 1.16
The ``internal_name`` and ``original_price`` fields have been added.
.. versionchanged:: 2.0
The field ``require_approval`` has been added.
The field ``internal_name`` and ``original_price`` fields have been added.
Notes
-----
@@ -167,7 +160,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -252,7 +244,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -317,7 +308,6 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -371,7 +361,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -456,7 +445,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},

View File

@@ -32,8 +32,8 @@ email string The customer em
locale string The locale used for communication with this customer
datetime datetime Time of order creation
expires datetime The order will expire, if it is still pending by this time
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
payment_date date Date of payment receipt
payment_provider string Payment provider used for this order
total money (string) Total value of this order
comment string Internal comment on this order
checkin_attention boolean If ``True``, the check-in app should show a warning
@@ -46,7 +46,6 @@ invoice_address object Invoice address
for orders created before pretix 1.7, do not rely on
it).
├ name string Customer name
├ name_parts object of strings Customer name decomposition
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
@@ -75,12 +74,6 @@ downloads list of objects List of ticket
download options.
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
require_approval boolean If ``True`` and the order is pending, this order
needs approval by an organizer before it can
continue. If ``True`` and the order is canceled,
this order has been denied by the event organizer.
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
===================================== ========================== =======================================================
@@ -115,12 +108,6 @@ last_modified datetime Last modificati
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
.. versionchanged:: 2.0
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. _order-position-resource:
Order position resource
@@ -138,7 +125,6 @@ item integer ID of the purch
variation integer ID of the purchased variation (or ``null``)
price money (string) Price of this position
attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
@@ -181,53 +167,9 @@ pdf_data object Data object req
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
.. _order-payment-resource:
Order payment resource
----------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
local_id integer Internal ID of this payment, starts at 1 for every order
state string Payment state, one of ``created``, ``pending``, ``confirmed``, ``canceled``, ``pending``, ``failed``, or ``refunded``
amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. _order-payment-resource:
Order refund resource
---------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
local_id integer Internal ID of this payment, starts at 1 for every order
state string Payment state, one of ``created``, ``transit``, ``external``, ``canceled``, ``failed``, or ``done``
source string How this refund has been created, one of ``buyer``, ``admin``, or ``external``
amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
List of all orders
------------------
Order endpoints
---------------
.. versionchanged:: 1.15
@@ -274,13 +216,11 @@ List of all orders
"total": "23.00",
"comment": "",
"checkin_attention": false,
"require_approval": false,
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"is_business": True,
"company": "Sample company",
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -298,9 +238,6 @@ List of all orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -338,18 +275,7 @@ List of all orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/download/pdf/"
}
],
"payments": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
],
"refunds": []
]
}
]
}
@@ -359,8 +285,6 @@ List of all orders
``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.
:query string email: Only return orders created with the given email address
:query string locale: Only return orders with the given customer locale
:query datetime modified_since: Only return orders that have changed since the given date
@@ -372,9 +296,6 @@ List of all orders
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual orders
--------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
Returns information on one order, identified by its order code.
@@ -410,13 +331,11 @@ Fetching individual orders
"total": "23.00",
"comment": "",
"checkin_attention": false,
"require_approval": false,
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"company": "Sample company",
"is_business": True,
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
@@ -434,9 +353,6 @@ Fetching individual orders
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -474,18 +390,7 @@ Fetching individual orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/download/pdf/"
}
],
"payments": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
],
"refunds": []
]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -496,9 +401,6 @@ Fetching individual orders
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
Order ticket download
---------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
@@ -540,9 +442,6 @@ Order ticket download
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Creating orders
---------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/
Creates a new order.
@@ -588,30 +487,28 @@ Creating orders
* ``code`` (optional)
* ``status`` (optional) Defaults to pending for non-free orders and paid for free orders. You can only set this to
``"n"`` for pending or ``"p"`` for paid. We will create a payment object for this order either in state ``created``
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
**not** be sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and
then call the ``mark_paid`` API method.
``"n"`` for pending or ``"p"`` for paid. If you create a paid order, the ``order_paid`` signal will **not** be
sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and then call
the ``mark_paid`` API method.
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas that become free by this operation will be credited to your order
creation.
* ``email``
* ``locale``
* ``payment_provider`` The identifier of the payment provider set for this order. This needs to be an existing
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
orders you create as paid.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
charge will be created), this is just informative in case you *handled the payment already*.
payment provider. You should use ``"free"`` for free orders.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``payment_info``
value of the order. How this value is handled is up to the payment provider and you should only use this if you
know the specific payment provider in detail. Please keep in mind that the payment provider will not be called
to do anything about this (i.e. if you pass a bank account to a debit provider, *no* charge will be created),
this is just informative in case you *handled the payment already*.
* ``comment`` (optional)
* ``checkin_attention`` (optional)
* ``invoice_address`` (optional)
* ``company``
* ``is_business``
* ``name`` **or** ``name_parts``
* ``name``
* ``street``
* ``zipcode``
* ``city``
@@ -625,7 +522,7 @@ Creating orders
* ``item``
* ``variation``
* ``price``
* ``attendee_name`` **or** ``attendee_name_parts``
* ``attendee_name``
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -674,7 +571,7 @@ Creating orders
"invoice_address": {
"is_business": False,
"company": "Sample company",
"name_parts": {"full_name": "John Doe"},
"name": "John Doe",
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
@@ -688,9 +585,7 @@ Creating orders
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"full_name": "Peter"
},
"attendee_name": "Peter",
"attendee_email": null,
"addon_to": null,
"answers": [
@@ -723,9 +618,6 @@ Creating orders
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
Order state operations
----------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
Marks a pending or expired order as successfully paid.
@@ -960,88 +852,9 @@ Order state operations
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/approve/
Approve an order that is pending approval.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/approve/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"code": "ABC12",
"status": "n",
"require_approval": false,
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to modify
:statuscode 200: no error
:statuscode 400: The order cannot be approved, likely because the current order status does not allow it.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/deny/
Marks an order that is pending approval as denied.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/deny/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: text/json
{
"send_email": true,
"comment": "You're not a business customer!"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"code": "ABC12",
"status": "c",
"require_approval": true,
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to modify
:statuscode 200: no error
:statuscode 400: The order cannot be marked as denied since the current order status does not allow it.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
List of all order positions
---------------------------
Order position endpoints
------------------------
.. versionchanged:: 1.15
@@ -1049,11 +862,6 @@ List of all order positions
``order__status__in``, ``subevent__in``, ``addon_to__in`` and ``search``. The search for attendee names and order
codes is now case-insensitive.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
Returns a list of all order positions within a given event.
@@ -1087,9 +895,6 @@ List of all order positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter"
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -1138,7 +943,6 @@ List of all order positions
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
products positions are shown if they refer to an attendee with the given name.
:query string secret: Only return positions with the given ticket secret.
:query string pseudonymization_id: Only return positions with the given pseudonymization ID.
:query string order__status: Only return positions with the given order status.
:query string order__status__in: Only return positions with one the given comma-separated order status.
:query boolean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
@@ -1148,17 +952,12 @@ List of all order positions
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
:query integer addon_to__in: Only return positions that are add-ons to one of the positions with the given
comma-separated IDs.
:query string voucher: Only return positions with a specific voucher.
:query string voucher__code: Only return positions with a specific voucher code.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Returns information on one order position, identified by its internal ID.
@@ -1187,9 +986,6 @@ Fetching individual positions
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
@@ -1230,9 +1026,6 @@ Fetching individual positions
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
Order position ticket download
------------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
Download tickets for one order position, identified by its internal ID.
@@ -1274,507 +1067,3 @@ Order position ticket download
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Manipulating individual positions
---------------------------------
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Deletes an order position, identified by its internal ID.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the order position to delete
:statuscode 204: no error
:statuscode 400: This position cannot be deleted (e.g. last position in order)
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
Order payment endpoints
-----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
Returns a list of all payments for an order.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/ 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": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/
Returns information on one payment, identified by its order-local ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/ 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
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/confirm/
Marks a payment as confirmed. Only allowed in states ``pending`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/confirm/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{"force": false}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "confirmed",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request or payment state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/cancel/
Marks a payment as canceled. Only allowed in states ``pending`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/cancel/ 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
{
"local_id": 1,
"state": "canceled",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request or payment state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/refund/
Create and execute a manual refund. Only available in ``confirmed`` state. Returns a refund resource, not
a payment resource!
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/refund/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"amount": "23.00",
"mark_refunded": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"source": "admin",
"state": "done",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request, payment state, or operation not supported by the payment provider
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
Order refund endpoints
----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Returns a list of all refunds for an order.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/ 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": [
{
"local_id": 1,
"state": "done",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/
Returns information on one refund, identified by its order-local ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/ 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
{
"local_id": 1,
"state": "done",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Creates a refund manually.
.. warning:: We recommend to only use this endpoint for refunds with payment provider ``manual``. This endpoint also
does not check for mismatching amounts etc. Be careful!
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"state": "created",
"source": "admin",
"amount": "23.00",
"payment": 1,
"execution_date": null,
"provider": "manual",
"mark_refunded": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "created",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": null,
"provider": "manual"
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 400: Invalid data supplied
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/done/
Marks a refund as completed. Only allowed in states ``transit`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/done/ 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
{
"local_id": 1,
"state": "done",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/process/
Acts on an external refund, either marks the order as refunded or pending. Only allowed in state ``external``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/done/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{"mark_refunded": false}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "done",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/cancel/
Marks a refund as canceled. Only allowed in states ``transit``, ``external``, and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/cancel/ 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
{
"local_id": 1,
"state": "canceled",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.

View File

@@ -128,7 +128,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content: application/json
{
"identifier": "LVETRWVU",

View File

@@ -17,7 +17,6 @@ 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
@@ -41,10 +40,6 @@ 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
---------
@@ -77,7 +72,6 @@ Endpoints
{
"id": 1,
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -98,10 +92,6 @@ 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
@@ -131,7 +121,6 @@ Endpoints
{
"id": 1,
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
@@ -155,63 +144,3 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view 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.

View File

@@ -231,76 +231,6 @@ Endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/vouchers/batch_create/
Creates multiple new vouchers atomically.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/vouchers/batch_create/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 408
[
{
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"valid_until": null,
"block_quota": false,
"allow_ignore_quota": false,
"price_mode": "set",
"value": "12.00",
"item": 1,
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
},
{
"code": "ASDKLJCYXCASDASD",
"max_usages": 1,
"valid_until": null,
"block_quota": false,
"allow_ignore_quota": false,
"price_mode": "set",
"value": "12.00",
"item": 1,
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
},
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
[
{
"id": 1,
"code": "43K6LKM37FBVR2YG",
}, …
}
:param organizer: The ``slug`` field of the organizer to create a vouchers for
:param event: The ``slug`` field of the event to create a vouchers for
:statuscode 201: no error
:statuscode 400: The vouchers could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of

View File

@@ -1,243 +0,0 @@
.. _`rest-webhooks`:
Webhooks
========
.. note:: This page is about how to modify webhook settings themselves through the REST API. If you just want to know
how webhooks work, go here: :ref:`webhooks`
Resource description
--------------------
The webhook resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the webhook
enabled boolean If ``False``, this webhook will not receive any notifications
target_url string The URL to call
all_events boolean If ``True``, this webhook will receive notifications
on all events of this organizer
limit_events list of strings If ``all_events`` is ``False``, this is a list of
event slugs this webhook is active for
action_types list of strings A list of action type filters that limit the
notifications sent to this webhook. See below for
valid values
===================================== ========================== =======================================================
The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.placed``
* ``pretix.event.order.paid``
* ``pretix.event.order.canceled``
* ``pretix.event.order.expired``
* ``pretix.event.order.modified``
* ``pretix.event.order.contact.changed``
* ``pretix.event.order.changed.*``
* ``pretix.event.order.refund.created.externally``
* ``pretix.event.order.refunded``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.checkin``
* ``pretix.event.checkin.reverted``
Installed plugins might register more valid values.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/webhooks/
Returns a list of all webhooks within a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/webhooks/ 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": 2,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer 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 this resource.
.. http:get:: /api/v1/organizers/(organizer)/webhooks/(id)/
Returns information on one webhook, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/webhooks/1/ 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
{
"id": 2,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the webhook 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 this resource.
.. http:post:: /api/v1/organizers/(organizer)/webhooks/
Creates a new webhook
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 3,
"enabled": true,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to create a webhook for
:statuscode 201: no error
:statuscode 400: The webhook could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/webhooks/(id)/
Update a webhook. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"enabled": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": false,
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the webhook to modify
:statuscode 200: no error
:statuscode 400: The webhook could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/webhook/(id)/
Delete a webhook. Currently, this will not delete but just disable the webhook.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the webhook to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.

View File

@@ -1,36 +0,0 @@
.. _`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

@@ -1,108 +0,0 @@
.. _`webhooks`:
Webhooks
========
pretix can send webhook calls to notify your application of any changes that happen inside pretix. This is especially
useful for everything triggered by an actual user, such as a new ticket sale or the arrival of a payment.
You can register any number of webhook URLs that pretix will notify any time one of the supported events occurs inside
your organizer account. A great example use case of webhooks would be to add the buyer to your mailing list every time
a new order comes in.
Configuring webhooks
--------------------
You can find the list of your active webhooks in the "Webhook" section of your organizer account:
.. thumbnail:: ../screens/organizer/webhook_list.png
:align: center
:class: screenshot
Click "Create webhook" if you want to add a new URL. You will then be able to enter the URL pretix shall call for
notifications. You need to select any number of notification types that you want to receive and you can optionally
filter the events you want to receive notifications for.
.. thumbnail:: ../screens/organizer/webhook_edit.png
:align: center
:class: screenshot
You can also configure webhooks :ref:`through the API itself <rest-webhooks>`.
Receiving webhooks
------------------
Creating a webhook endpoint on your server is no different from creating any other page on your website. If your
website is written in PHP, you might just create a new ``.php`` file on your server; if you use a web framework like
Symfony or Django, you would just create a new route with the desired URL.
We will call your URL with a HTTP ``POST`` request with a ``JSON`` body. In PHP, you can parse this like this::
$input = @file_get_contents('php://input');
$event_json = json_decode($input);
// Do something with $event_json
In Django, you would create a view like this::
def my_webhook_view(request):
event_json = json.loads(request.body)
# Do something with event_json
return HttpResponse(status=200)
More samples for the language of your choice are easy to find online.
The exact body of the request varies by notification type, but for the main types included with pretix core, such as
those related to changes of an order, it will look like this::
{
"notification_id": 123455,
"organizer": "acmecorp",
"event": "democon",
"code": "ABC23",
"action": "pretix.event.order.placed"
}
Notifications regarding a check-in will contain more details like ``orderposition_id``
and ``checkin_list``.
.. warning:: You should not trust data supplied to your webhook, but only use it as a trigger to fetch updated data.
Anyone could send data there if they guess the correct URL and you won't be able to tell. Therefore, we
only include the minimum amount of data necessary for you to fetch the changed objects from our
:ref:`rest-api` in an authenticated way.
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require
that a valid certificate is in use.
.. note:: If you use a web framework that makes use of automatic CSRF protection, this protection might prevent us
from calling your webhook URL. In this case, we recommend that you turn of CSRF protection selectively
for that route. In Django, you can do this by putting the ``@csrf_exempt`` decorator on your view. In
Rails, you can pass an ``except`` parameter to ``protect_from_forgery``.
Responding to a webhook
-----------------------
If you successfully received a webhook call, your endpoint should return a HTTP status code between ``200`` and ``299``.
If any other status code is returned, we will assume you did not receive the call. This does mean that any redirection
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
headers and pretix will ignore all other information in your response headers or body.
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
multiple times for the same event due to a perceived error does not do any harm.
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
endpoint does not exist any more and automatically disable the webhook.
.. note:: If you use a self-hosted version of pretix (i.e. not our SaaS offering at pretix.eu) and you did not
configure a background task queue, failed webhooks will not be retried.
Debugging webhooks
------------------
If you want to debug your webhooks, you can view a log of all sent notifications and the responses of your server for
30 days right next to your configuration.
.. _Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication

View File

@@ -64,7 +64,7 @@ Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionR
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
from django.urls import resolve, reverse
from django.core.urlresolvers import resolve, reverse
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.control.signals import nav_event

View File

@@ -1,109 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Writing an HTML e-mail renderer plugin
======================================
An email renderer class controls how the HTML part of e-mails sent by pretix is built.
The creation of such a plugin is very similar to creating an export output.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Output registration
-------------------
The email HTML renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email renderers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.email.BaseHTMLMailRenderer``
that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_html_mail_renderers
@receiver(register_html_mail_renderers, dispatch_uid="renderer_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyMailRenderer
return MyMailRenderer
The renderer class
------------------
.. class:: pretix.base.email.BaseHTMLMailRenderer
The central object of each email renderer is the subclass of ``BaseHTMLMailRenderer``.
.. py:attribute:: BaseHTMLMailRenderer.event
The default constructor sets this property to the event we are currently
working for.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. autoattribute:: thumbnail_filename
This is an abstract attribute, you **must** override this!
.. autoattribute:: is_available
.. automethod:: render
This is an abstract method, you **must** implement this!
Helper class for template-base renderers
----------------------------------------
The email renderer that ships with pretix is based on Django templates to generate HTML.
In case you also want to render emails based on a template, we provided a ready-made base
class ``TemplateBasedMailRenderer`` that you can re-use to perform the following steps:
* Convert the body text and the signature to HTML using our markdown renderer
* Render the template
* Call `inlinestyler`_ to convert all ``<style>`` style sheets to inline ``style=""``
attributes for better compatibility
To use it, you just need to implement some variables::
class ClassicMailRenderer(TemplateBasedMailRenderer):
verbose_name = _('pretix default')
identifier = 'classic'
thumbnail_filename = 'pretixbase/email/thumb.png'
template_name = 'pretixbase/email/plainwrapper.html'
The template is passed the following context variables:
``site``
Name of the pretix installation (``settings.PRETIX_INSTANCE_NAME``)
``site_url``
Root URL of the pretix installation (``settings.SITE_URL``)
``body``
The body as markdown (render with ``{{ body|safe }}``)
``subject``
The email subject
``color``
The primary color of the event
``event``
The ``Event`` object
``signature`` (optional, only if configured)
The body as markdown (render with ``{{ signature|safe }}``)
``order`` (optional, only if applicable)
The ``Order`` object
.. _inlinestyler: https://pypi.org/project/inlinestyler/

View File

@@ -48,8 +48,7 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, event_settings_widget, oauth_application_registered
.. automodule:: pretix.base.signals

View File

@@ -10,8 +10,6 @@ Contents:
exporter
ticketoutput
payment
payment_2.0
email
invoice
shredder
customview

View File

@@ -9,10 +9,6 @@ is very similar to creating an export output.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
.. warning:: We changed our payment provider API a lot in pretix 2.x. Our documentation page on :ref:`payment2.0`
might be insightful even if you do not have a payment provider to port, as it outlines the rationale
behind the current design.
Provider registration
---------------------
@@ -35,7 +31,7 @@ that the plugin will provide::
The provider class
------------------
.. py:class:: pretix.base.payment.BasePaymentProvider
.. class:: pretix.base.payment.BasePaymentProvider
The central object of each payment provider is the subclass of ``BasePaymentProvider``.
@@ -58,62 +54,58 @@ The provider class
This is an abstract attribute, you **must** override this!
.. autoattribute:: public_name
.. autoattribute:: is_enabled
.. automethod:: calculate_fee
.. autoattribute:: settings_form_fields
.. automethod:: settings_form_clean
.. automethod:: settings_content_render
.. automethod:: is_allowed
.. automethod:: render_invoice_text
.. automethod:: payment_form_render
.. automethod:: payment_form
.. automethod:: is_allowed
.. autoattribute:: payment_form_fields
.. automethod:: payment_is_valid_session
.. automethod:: checkout_prepare
.. automethod:: payment_is_valid_session
.. automethod:: checkout_confirm_render
This is an abstract method, you **must** override this!
.. automethod:: execute_payment
.. automethod:: calculate_fee
.. automethod:: payment_perform
.. automethod:: order_pending_mail_render
.. automethod:: payment_pending_render
.. automethod:: order_pending_render
.. autoattribute:: abort_pending_allowed
.. automethod:: render_invoice_text
This is an abstract method, you **must** override this!
.. automethod:: order_change_allowed
.. automethod:: payment_prepare
.. automethod:: order_can_retry
.. automethod:: payment_control_render
.. automethod:: order_prepare
.. automethod:: payment_refund_supported
.. automethod:: order_paid_render
.. automethod:: payment_partial_refund_supported
.. automethod:: order_control_render
.. automethod:: execute_refund
.. automethod:: order_control_refund_render
.. automethod:: order_control_refund_perform
.. automethod:: is_implicit
.. automethod:: shred_payment_info
.. autoattribute:: is_implicit
.. autoattribute:: is_meta
Additional views
----------------

View File

@@ -1,129 +0,0 @@
.. highlight:: python
:linenothreshold: 5
.. _`payment2.0`:
Porting a payment provider from pretix 1.x to pretix 2.x
========================================================
In pretix 2.x, we changed large parts of the payment provider API. This documentation details the changes we made
and shows you how you can make an existing pretix 1.x payment provider compatible with pretix 2.x
Conceptual overview
-------------------
In pretix 1.x, an order was always directly connected to a payment provider for the full life of an order. As long as
an order was unpaid, this could still be changed in some cases, but once an order was paid, no changes to the payment
provider were possible any more. Additionally, the internal state of orders allowed orders only to be fully paid or
not paid at all. This leads to a couple of consequences:
* Payment-related functions (like "execute payment" or "do a refund") always operated on full orders.
* Changing the total of an order was basically impossible once an order was paid, since there was no concept of
partial payments or partial refunds.
* Payment provider plugins needed to take complicated steps to detect cases that require human intervention, like e.g.
* An order has expired, no quota is left to revive it, but a payment has been received
* A payment has been received for a canceled order
* A payment has been received for an order that has already been paid with a different payment method
* An external payment service notified us of a refund/dispute
We noticed that we copied and repeated large portions of code in all our official payment provider plugins, just
to deal with some of these cases.
* Sometimes, there is the need to mark an order as refunded within pretix, without automatically triggering a refund
with an external API. Every payment method needed to implement a user interface for this independently.
* If a refund was not possible automatically, there was no way user to track which payments actually have been refunded
manually and which are still left to do.
* When the payment with one payment provider failed and the user changed to a different payment provider, all
information about the first payment was lost from the order object and could only be retrieved from order log data,
which also made it hard to design a data shredder API to get rid of this data.
In pretix 2.x, we introduced two new models, :py:class:`OrderPayment <pretix.base.models.OrderPayment>` and
:py:class:`OrderRefund <pretix.base.models.OrderRefund>`. Each instance of these is connected to an order and
represents one single attempt to pay or refund a specific amount of money. Each one of these has an individual state,
can individually fail or succeed, and carries an amount variable that can differ from the order total.
This has the following advantages:
* The system can now detect orders that are over- or underpaid, independent of the payment providers in use.
* Therefore, we can now allow partial payments, partial refunds, and changing paid orders, and automatically detect
the cases listed above and notify the user.
Payment providers now interact with those payment and refund objects more than with orders.
Your to-do list
---------------
Payment processing
""""""""""""""""""
* The method ``BasePaymentProvider.order_pending_render`` has been removed and replaced by a new
``BasePaymentProvider.payment_pending_render(request, payment)`` method that is passed an ``OrderPayment``
object instead of an ``Order``.
* The method ``BasePaymentProvider.payment_form_render`` now receives a new ``total`` parameter.
* The method ``BasePaymentProvider.payment_perform`` has been removed and replaced by a new method
``BasePaymentProvider.execute_payment(request, payment)`` that is passed an ``OrderPayment``
object instead of an ``Order``.
* The function ``pretix.base.services.mark_order_paid`` has been removed, instead call ``payment.confirm()``
on a pending ``OrderPayment`` object. If no further payments are required for this order, this will also
mark the order as paid automatically. Note that ``payment.confirm()`` can still throw a ``QuotaExceededException``,
however it will still mark the payment as complete (not the order!), so you should catch this exception and
inform the user, but not abort the transaction.
* A new property ``BasePaymentProvider.abort_pending_allowed`` has been introduced. Only if set, the user will
be able to retry a payment or switch the payment method when the order currently has a payment object in
state ``"pending"``. This replaces ``BasePaymentProvider.order_can_retry``, which no longer exists.
* The methods ``BasePaymentProvider.retry_prepare`` and ``BasePaymentProvider.order_prepare`` have both been
replaced by a new method ``BasePaymentProvider.payment_prepare(request, payment)`` that is passed an ``OrderPayment``
object instead of an ``Order``. **Keep in mind that this payment object might have an amount property that
differs from the order total, if the order is already partially paid.**
* The method ``BasePaymentProvider.order_paid_render`` has been removed.
* The method ``BasePaymentProvider.order_control_render`` has been removed and replaced by a new method
``BasePaymentProvider.payment_control_render(request, payment)`` that is passed an ``OrderPayment``
object instead of an ``Order``.
* There's no need to manually deal with excess payments or duplicate payments anymore, just setting the ``OrderPayment``
methods to the correct state will do the job.
Creating refunds
""""""""""""""""
* The methods ``BasePaymentProvider.order_control_refund_render`` and ``BasePaymentProvider.order_control_refund_perform``
have been removed.
* Two new boolean methods ``BasePaymentProvider.payment_refund_supported(payment)`` and ``BasePaymentProvider.payment_partial_refund_supported(payment)``
have been introduced. They should be set to return ``True`` if and only if the payment API allows to *automatically*
transfer the money back to the customer.
* A new method ``BasePaymentProvider.execute_refund(refund)`` has been introduced. This method is called using a
``OrderRefund`` object in ``"created"`` state and is expected to transfer the money back and confirm success with
calling ``refund.done()``. This will only ever be called if either ``BasePaymentProvider.payment_refund_supported(payment)``
or ``BasePaymentProvider.payment_partial_refund_supported(payment)`` return ``True``.
Processing external refunds
"""""""""""""""""""""""""""
* If e.g. a webhook API notifies you that a payment has been disputed or refunded with the external API, you are
expected to call ``OrderPayment.create_external_refund(self, amount, execution_date, info='{}')`` on this payment.
This will create and return an appropriate ``OrderRefund`` object and send out a notification. However, it will not
mark the order as refunded, but will ask the event organizer for a decision.
Data shredders
""""""""""""""
* The method ``BasePaymentProvider.shred_payment_info`` is no longer passed an order, but instead **either**
an ``OrderPayment`` **or** an ``OrderRefund``.

View File

@@ -86,15 +86,6 @@ Carts and Orders
.. autoclass:: pretix.base.models.OrderPosition
:members:
.. autoclass:: pretix.base.models.OrderFee
:members:
.. autoclass:: pretix.base.models.OrderPayment
:members:
.. autoclass:: pretix.base.models.OrderRefund
:members:
.. autoclass:: pretix.base.models.CartPosition
:members:

View File

@@ -18,7 +18,7 @@ External Dependencies
---------------------
Your should install the following on your system:
* Python 3.5 or newer
* Python 3.4 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* ``libffi`` (Debian package: ``libffi-dev``)
@@ -54,6 +54,10 @@ The first thing you need are all the main application's dependencies::
cd src/
pip3 install -r requirements.txt -r requirements/dev.txt
If you are working with Python 3.4, you will also need (you can skip this for Python 3.5+)::
pip3 install -r requirements/py34.txt
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
python manage.py collectstatic --noinput

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -23,7 +23,6 @@ cronjob
cryptographic
debian
deduplication
deprovision
discoverable
django
dockerfile
@@ -89,7 +88,6 @@ regex
renderer
renderers
reportlab
SaaS
screenshot
selectable
serializers
@@ -106,7 +104,6 @@ subevent
subevents
submodule
subpath
Symfony
systemd
testutils
timestamp

View File

@@ -107,13 +107,6 @@ voucher's settings.
</div>
</noscript>
Disabling the voucher input
---------------------------
If you want to disable voucher input in the widget, you can pass the ``disable-vouchers`` attribute::
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
pretix Button
-------------
@@ -143,7 +136,7 @@ resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button``
As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
items, if the items have variations. If you omit the ``items`` attribute, the general start page will be presented.
items, if the items have variations.
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.

View File

@@ -1,6 +0,0 @@
build:
image: latest
python:
version: 3.6

12
src/.coveragerc Normal file
View File

@@ -0,0 +1,12 @@
[run]
source = pretix
omit = */migrations/*,*/urls.py,*/tests/*,*/testdummy/*,*/admin.py,pretix/wsgi.py,pretix/settings.py
[report]
exclude_lines =
pragma: no cover
def __str__
der __repr__
if settings.DEBUG
NOQA
NotImplementedError

View File

@@ -8,8 +8,6 @@ recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *
recursive-include pretix/plugins/banktransfer/templates *
recursive-include pretix/plugins/banktransfer/static *
recursive-include pretix/plugins/manualpayment/templates *
recursive-include pretix/plugins/manualpayment/static *
recursive-include pretix/plugins/paypal/templates *
recursive-include pretix/plugins/pretixdroid/templates *
recursive-include pretix/plugins/pretixdroid/static *

View File

@@ -1 +1 @@
__version__ = "2.2.0"
__version__ = "2.0.0.dev0"

View File

@@ -5,8 +5,5 @@ class PretixApiConfig(AppConfig):
name = 'pretix.api'
label = 'pretixapi'
def ready(self):
from . import signals, webhooks # noqa
default_app_config = 'pretix.api.PretixApiConfig'

View File

@@ -1,25 +0,0 @@
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 Device, Event
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
@@ -9,9 +9,10 @@ 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, (Device, TeamAPIToken)):
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
return False
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
@@ -30,7 +31,7 @@ class EventPermission(BasePermission):
except SessionReauthRequired:
return False
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
request.event = Event.objects.filter(
@@ -75,7 +76,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 ['update', 'partial_update'] \
elif view.action in ['retrieve', 'update', 'partial_update'] \
and 'can_change_event_settings' not in request.eventpermset:
return False

View File

@@ -10,10 +10,7 @@ def custom_exception_handler(exc, context):
if isinstance(exc, LockTimeoutException):
response = Response(
{'detail': 'The server was too busy to process your request. Please try again.'},
status=status.HTTP_409_CONFLICT,
headers={
'Retry-After': 5
}
status=status.HTTP_409_CONFLICT
)
return response

View File

@@ -46,7 +46,7 @@ class Migration(migrations.Migration):
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255, verbose_name='Application name')),
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated',
validators=[oauth2_provider.validators.URIValidator],
validators=[oauth2_provider.validators.validate_uris],
verbose_name='Redirection URIs')),
('client_id',
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100,

View File

@@ -1,79 +0,0 @@
# Generated by Django 2.1.1 on 2018-11-07 10:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0102_auto_20181017_0024'),
('pretixapi', '0002_auto_20180604_1120'),
]
operations = [
migrations.CreateModel(
name='WebHook',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('enabled', models.BooleanField(default=True, verbose_name='Enable webhook')),
('target_url', models.URLField(verbose_name='Target URL')),
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer')),
],
),
migrations.CreateModel(
name='WebHookCall',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('target_url', models.URLField()),
('is_retry', models.BooleanField(default=False)),
('execution_time', models.FloatField(null=True)),
('return_code', models.PositiveIntegerField(default=0)),
('payload', models.TextField()),
('response_body', models.TextField()),
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
],
),
migrations.CreateModel(
name='WebHookEventListener',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
],
),
migrations.AddField(
model_name='webhookcall',
name='success',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='webhook',
name='all_events',
field=models.BooleanField(default=True, verbose_name='All events (including newly created ones)'),
),
migrations.AlterField(
model_name='webhook',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='pretixbase.Organizer'),
),
migrations.AlterField(
model_name='webhookcall',
name='webhook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='pretixapi.WebHook'),
),
migrations.AlterField(
model_name='webhookeventlistener',
name='webhook',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='listeners', to='pretixapi.WebHook'),
),
migrations.AddField(
model_name='webhookcall',
name='action_type',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -11,13 +11,13 @@ from oauth2_provider.models import (
AbstractAccessToken, AbstractApplication, AbstractGrant,
AbstractRefreshToken,
)
from oauth2_provider.validators import URIValidator
from oauth2_provider.validators import validate_uris
class OAuthApplication(AbstractApplication):
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
redirect_uris = models.TextField(
blank=False, validators=[URIValidator],
blank=False, validators=[validate_uris],
verbose_name=_("Redirection URIs"),
help_text=_("Allowed URIs list, space separated")
)
@@ -68,41 +68,3 @@ class OAuthRefreshToken(AbstractRefreshToken):
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
related_name="refresh_token"
)
class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
target_url = models.URLField(verbose_name=_("Target URL"))
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
@property
def action_types(self):
return [
l.action_type for l in self.listeners.all()
]
class WebHookEventListener(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='listeners')
action_type = models.CharField(max_length=255)
class Meta:
ordering = ("action_type",)
class WebHookCall(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
datetime = models.DateTimeField(auto_now_add=True)
target_url = models.URLField()
action_type = models.CharField(max_length=255)
is_retry = models.BooleanField(default=False)
execution_time = models.FloatField(null=True)
return_code = models.PositiveIntegerField(default=0)
success = models.BooleanField(default=False)
payload = models.TextField()
response_body = models.TextField()
class Meta:
ordering = ("-datetime",)

View File

@@ -19,19 +19,18 @@ class CartPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = CartPosition
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers',)
class CartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers',)
def create(self, validated_data):
@@ -66,11 +65,6 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
quota.name
)
)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
@@ -124,8 +118,4 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'You cannot specify a variation for this item.'
)
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
return data

View File

@@ -4,7 +4,6 @@ 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
@@ -191,13 +190,12 @@ 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', 'event',
'presale_start', 'presale_end', 'location',
'item_price_overrides', 'variation_price_overrides', 'meta_data')

View File

@@ -79,7 +79,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
'variations', 'addons', 'original_price', 'require_approval')
'variations', 'addons', 'original_price')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):

View File

@@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy
from django_countries.fields import Country
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -15,9 +14,7 @@ from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
)
from pretix.base.models.orders import CartPosition, OrderFee
from pretix.base.pdf import get_variables
from pretix.base.signals import register_ticket_outputs
@@ -35,12 +32,11 @@ class CompatibleCountryField(serializers.Field):
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'vat_id', 'vat_id_validated', 'internal_reference')
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs):
@@ -49,15 +45,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
v.required = False
v.allow_blank = True
def validate(self, data):
if data.get('name') and data.get('name_parts'):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
return data
class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
@@ -87,8 +74,7 @@ class CheckinSerializer(I18nAwareModelSerializer):
class OrderDownloadsField(serializers.Field):
def to_representation(self, instance: Order):
if instance.status != Order.STATUS_PAID:
if instance.status != Order.STATUS_PENDING or instance.require_approval or not instance.event.settings.ticket_download_pending:
return []
return []
request = self.context['request']
res = []
@@ -111,8 +97,7 @@ class OrderDownloadsField(serializers.Field):
class PositionDownloadsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
if instance.order.status != Order.STATUS_PAID:
if instance.order.status != Order.STATUS_PENDING or instance.order.require_approval or not instance.order.event.settings.ticket_download_pending:
return []
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:
@@ -141,19 +126,12 @@ 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.
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
for k, f in self.context['vars'].items():
pdfvars = get_variables(instance.order.event)
for k, f in pdfvars.items():
res[k] = f['evaluate'](instance, instance.order, ev)
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
for k, v in ev._cached_meta_data.items():
for k, v in ev.meta_data.items():
res['meta:' + k] = v
return res
@@ -168,9 +146,9 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -178,61 +156,23 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
self.fields.pop('pdf_data')
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.provider
return t
class OrderPaymentDateField(serializers.DateField):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.payment_date or t
if t:
return super().to_representation(t.date())
class OrderFeeSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class OrderPaymentSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPayment
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
class OrderRefundSerializer(I18nAwareModelSerializer):
payment = SlugRelatedField(slug_field='local_id', read_only=True)
class Meta:
model = OrderRefund
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer()
positions = OrderPositionSerializer(many=True)
fees = OrderFeeSerializer(many=True)
downloads = OrderDownloadsField(source='*')
payments = OrderPaymentSerializer(many=True)
refunds = OrderRefundSerializer(many=True)
payment_date = OrderPaymentDateField(source='*')
payment_provider = OrderPaymentTypeField(source='*')
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval')
'checkin_attention', 'last_modified')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -315,11 +255,10 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret):
@@ -370,12 +309,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'variation': ['You cannot specify a variation for this item.']}
)
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
return data
@@ -477,17 +410,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
payment_provider = validated_data.pop('payment_provider')
payment_info = validated_data.pop('payment_info', '{}')
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
name = iadata.pop('name', '')
if name and not iadata.get('name_parts'):
iadata['name_parts'] = {
'_legacy': name
}
ia = InvoiceAddress(**iadata)
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
else:
ia = None
@@ -539,50 +463,25 @@ 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.set_expires(subevents=[p['subevent'] for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
order.meta_info = "{}"
order.save()
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.payment_provider = 'free'
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
elif order.payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
elif validated_data.get('status') == Order.STATUS_PAID:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=now(),
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
if validated_data.get('status') == Order.STATUS_PAID:
order.payment_date = now()
order.save()
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
addon_to = pos_data.pop('addon_to', None)
attendee_name = pos_data.pop('attendee_name', '')
if attendee_name and not pos_data.get('attendee_name_parts'):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
answers_data = pos_data.pop('answers')
addon_to = pos_data.pop('addon_to')
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
@@ -591,7 +490,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
options = answ_data.pop('options', [])
options = answ_data.pop('options')
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
@@ -623,27 +522,3 @@ class InvoiceSerializer(I18nAwareModelSerializer):
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference')
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
payment = serializers.IntegerField(required=False, allow_null=True)
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
info = CompatibleJSONField(required=False)
class Meta:
model = OrderRefund
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
def create(self, validated_data):
pid = validated_data.pop('payment', None)
if pid:
try:
p = self.context['order'].payments.get(local_id=pid)
except OrderPayment.DoesNotExist:
raise ValidationError('Unknown payment ID.')
else:
p = None
order = OrderRefund(order=self.context['order'], payment=p, **validated_data)
order.save()
return order

View File

@@ -1,27 +1,7 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Voucher
class VoucherListSerializer(serializers.ListSerializer):
def create(self, validated_data):
codes = set()
errs = []
err = False
for voucher_data in validated_data:
if voucher_data['code'] in codes:
err = True
errs.append({'code': ['Duplicate voucher code in request.']})
else:
codes.add(voucher_data['code'])
errs.append({})
if err:
raise ValidationError(errs)
return super().create(validated_data)
class VoucherSerializer(I18nAwareModelSerializer):
class Meta:
model = Voucher
@@ -29,7 +9,6 @@ class VoucherSerializer(I18nAwareModelSerializer):
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer
def validate(self, data):
data = super().validate(data)

View File

@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent')
read_only_fields = ('id', 'created', 'voucher')
def validate(self, data):

View File

@@ -1,71 +0,0 @@
from django.core.exceptions import ValidationError
from rest_framework import serializers
from pretix.api.models import WebHook
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.models import Event
class EventRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return self.context['organizer'].events.all()
class ActionTypesField(serializers.Field):
def to_representation(self, instance: WebHook):
return instance.action_types
def to_internal_value(self, data):
types = get_all_webhook_events()
for d in data:
if d not in types:
raise ValidationError('Invalid action type "%s".' % d)
return {'action_types': data}
class WebHookSerializer(I18nAwareModelSerializer):
limit_events = EventRelatedField(
slug_field='slug',
queryset=Event.objects.none(),
many=True
)
action_types = ActionTypesField(source='*')
class Meta:
model = WebHook
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
for event in full_data.get('limit_events'):
if self.context['organizer'] != event.organizer:
raise ValidationError('One or more events do not belong to this organizer.')
if full_data.get('limit_events') and full_data.get('all_events'):
raise ValidationError('You can set either limit_events or all_events.')
return data
def create(self, validated_data):
action_types = validated_data.pop('action_types')
inst = super().create(validated_data)
for l in action_types:
inst.listeners.create(action_type=l)
return inst
def update(self, instance, validated_data):
action_types = validated_data.pop('action_types', None)
instance = super().update(instance, validated_data)
if action_types is not None:
current_listeners = set(instance.listeners.values_list('action_type', flat=True))
new_listeners = set(action_types)
for l in current_listeners - new_listeners:
instance.listeners.filter(action_type=l).delete()
for l in new_listeners - current_listeners:
instance.listeners.create(action_type=l)
return instance

View File

@@ -1,21 +0,0 @@
from datetime import timedelta
from django.dispatch import Signal, receiver
from django.utils.timezone import now
from pretix.api.models import WebHookCall
from pretix.base.signals import periodic_task
register_webhook_events = Signal(
providing_args=[]
)
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instances.
"""
@receiver(periodic_task)
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()

View File

@@ -7,8 +7,7 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, device, event, item, oauth, order, organizer, user, voucher,
waitinglist, webhooks,
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
)
router = routers.DefaultRouter()
@@ -16,8 +15,6 @@ router.register(r'organizers', organizer.OrganizerViewSet)
orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
@@ -45,10 +42,6 @@ item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet)
order_router.register(r'refunds', order.RefundViewSet)
# Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
@@ -64,13 +57,7 @@ urlpatterns = [
include(question_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
include(checkinlist_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/orders/(?P<order>[^/]+)/', include(order_router.urls)),
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"),
url(r"^me$", user.MeView.as_view(), name="user.me"),
]

View File

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

@@ -1,7 +1,6 @@
from django.core.exceptions import ValidationError
from django.db.models import Count, F, Max, OuterRef, Prefetch, Subquery
from django.db.models.functions import Coalesce
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -33,7 +32,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_class = CheckinListFilter
filter_class = CheckinListFilter
permission = 'can_view_orders'
write_permission = 'can_change_event_settings'
@@ -154,7 +153,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name_cached', 'positionid')
ordering = ('attendee_name', 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
@@ -162,11 +161,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
},
'-attendee_name': {
'_order': F('display_name').desc(nulls_last=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
},
'last_checked_in': {
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
@@ -176,16 +175,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
},
}
filterset_class = CheckinOrderPositionFilter
filter_class = CheckinOrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
@cached_property
def checkinlist(self):
try:
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
except ValueError:
raise Http404()
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
def get_queryset(self):
cqs = Checkin.objects.filter(
@@ -244,9 +240,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=dt,
questions_supported=self.request.data.get('questions_supported', True),
user=self.request.user,
auth=self.request.auth,
questions_supported=self.request.data.get('questions_supported', True)
)
except RequiredQuestionsError as e:
return Response({

View File

@@ -1,113 +0,0 @@
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,7 +1,5 @@
import django_filters
from django.db import transaction
from django.db.models import ProtectedError, Q
from django.utils.timezone import now
from django.db.models import ProtectedError
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
@@ -12,79 +10,20 @@ from pretix.api.serializers.event import (
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
)
from pretix.base.models import Event, ItemCategory, TaxRule
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):
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'
)
return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property')
def perform_update(self, serializer):
current_live_value = serializer.instance.live
@@ -181,62 +120,19 @@ 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', '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)
fields = ['active']
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = SubEventFilter
filter_class = SubEventFilter
def get_queryset(self):
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(
return self.request.event.subevents.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set'
)

View File

@@ -41,8 +41,8 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filterset_class = ItemFilter
permission = None
filter_class = ItemFilter
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
@@ -83,7 +83,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
user=self.request.user,
auth=self.request.auth,
)
self.get_object().cartposition_set.all().delete()
super().perform_destroy(instance)
@@ -93,7 +92,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
@@ -155,7 +154,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
@@ -208,10 +207,10 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemCategorySerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = ItemCategoryFilter
filter_class = ItemCategoryFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = None
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
@@ -262,11 +261,10 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = QuestionFilter
filter_class = QuestionFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = None
write_permission = 'can_change_items'
permission = 'can_change_items'
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
@@ -309,7 +307,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = None
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
@@ -361,10 +359,10 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filterset_class = QuotaFilter
filter_class = QuotaFilter
ordering_fields = ('id', 'size')
ordering = ('id',)
permission = None
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):

View File

@@ -3,13 +3,12 @@ import datetime
import django_filters
import pytz
from django.db import transaction
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.db.models import Q
from django.db.models.functions import Concat
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import mixins, serializers, status, viewsets
from rest_framework import serializers, status, viewsets
from rest_framework.decorators import detail_route
from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
@@ -20,23 +19,20 @@ from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer,
InvoiceSerializer, OrderCreateSerializer, OrderPositionSerializer,
OrderSerializer,
)
from pretix.base.models import (
Device, Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken,
Invoice, Order, OrderPosition, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
OrderError, cancel_order, extend_order, mark_order_expired,
mark_order_paid, mark_order_refunded,
)
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
@@ -45,14 +41,14 @@ from pretix.base.signals import order_placed, register_ticket_outputs
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
email = django_filters.CharFilter(name='email', lookup_expr='iexact')
code = django_filters.CharFilter(name='code', lookup_expr='iexact')
status = django_filters.CharFilter(name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(name='last_modified', lookup_expr='gte')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'require_approval']
fields = ['code', 'status', 'email', 'locale']
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
@@ -60,8 +56,8 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter
ordering_fields = ('datetime', 'code', 'status')
filter_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
@@ -72,34 +68,13 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return ctx
def get_queryset(self):
qs = self.request.event.orders.prefetch_related(
'fees', 'payments', 'refunds', 'refunds__payment'
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'positions__answers__question', 'fees'
).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:
@@ -147,33 +122,14 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order = self.get_object()
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
ps = order.pending_sum
try:
p = order.payments.get(
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
provider='manual',
amount=ps
mark_order_paid(
order, manual=True,
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
except OrderPayment.DoesNotExist:
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider='manual',
amount=ps,
fee=None
)
try:
p.confirm(auth=self.request.auth,
user=self.request.user if request.user.is_authenticated else None,
count_waitinglist=False)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
@@ -198,48 +154,11 @@ 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
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
order = self.get_object()
try:
approve_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except OrderError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def deny(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
comment = request.data.get('comment', '')
order = self.get_object()
try:
deny_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail,
comment=comment,
)
except OrderError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def mark_pending(self, request, **kwargs):
order = self.get_object()
@@ -251,7 +170,8 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
)
order.status = Order.STATUS_PENDING
order.save(update_fields=['status'])
order.payment_manual = True
order.save()
order.log_action(
'pretix.event.order.unpaid',
user=request.user if request.user.is_authenticated else None,
@@ -289,7 +209,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_order_refunded(
order,
user=request.user if request.user.is_authenticated else None,
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
return self.retrieve(request, [], **kwargs)
@@ -365,7 +285,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
search = django_filters.CharFilter(method='search_qs')
@@ -373,17 +293,17 @@ class OrderPositionFilter(FilterSet):
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(attendee_name__icontains=value)
| Q(addon_to__attendee_name__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__invoice_address__name__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
return queryset.filter(Q(attendee_name__iexact=value) | Q(addon_to__attendee_name__iexact=value))
class Meta:
model = OrderPosition
@@ -393,32 +313,18 @@ class OrderPositionFilter(FilterSet):
'secret': ['exact'],
'order__status': ['exact', 'in'],
'addon_to': ['exact', 'in'],
'subevent': ['exact', 'in'],
'pseudonymization_id': ['exact'],
'voucher__code': ['exact'],
'voucher': ['exact'],
'subevent': ['exact', 'in']
}
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter
filter_class = OrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'-attendee_name': {
'_order': F('display_name').asc(nulls_last=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
}
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
@@ -459,232 +365,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
)
return resp
def perform_destroy(self, instance):
try:
ocm = OrderChangeManager(
instance.order,
user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth,
notify=False
)
ocm.cancel(instance)
ocm.commit()
except OrderError as e:
raise ValidationError(str(e))
except Quota.QuotaExceededException as e:
raise ValidationError(str(e))
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer
queryset = OrderPayment.objects.none()
permission = 'can_view_orders'
write_permission = 'can_change_orders'
lookup_field = 'local_id'
def get_queryset(self):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.payments.all()
@detail_route(methods=['POST'])
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
try:
payment.confirm(user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth,
count_waitinglist=False,
force=force)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def refund(self, request, **kwargs):
payment = self.get_object()
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('amount', str(payment.amount))
)
mark_refunded = request.data.get('mark_refunded', False)
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
full_refund_possible = payment.payment_provider.payment_refund_supported(payment)
partial_refund_possible = payment.payment_provider.payment_partial_refund_supported(payment)
available_amount = payment.amount - payment.refunded_amount
if amount <= 0:
return Response({'amount': ['Invalid refund amount.']}, status=status.HTTP_400_BAD_REQUEST)
if amount > available_amount:
return Response(
{'amount': ['Invalid refund amount, only {} are available to refund.'.format(available_amount)]},
status=status.HTTP_400_BAD_REQUEST)
if amount != payment.amount and not partial_refund_possible:
return Response({'amount': ['Partial refund not available for this payment method.']},
status=status.HTTP_400_BAD_REQUEST)
if amount == payment.amount and not full_refund_possible:
return Response({'amount': ['Full refund not available for this payment method.']},
status=status.HTTP_400_BAD_REQUEST)
r = payment.order.refunds.create(
payment=payment,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
amount=amount,
provider=payment.provider
)
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
return Response({'detail': 'External error: {}'.format(str(e))},
status=status.HTTP_400_BAD_REQUEST)
else:
payment.order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
if payment.order.pending_sum > 0:
if mark_refunded:
mark_order_refunded(payment.order,
user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth)
else:
payment.order.status = Order.STATUS_PENDING
payment.order.set_expires(
now(),
payment.order.event.subevents.filter(
id__in=payment.order.positions.values_list('subevent_id', flat=True))
)
payment.order.save(update_fields=['status', 'expires'])
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
@detail_route(methods=['POST'])
def cancel(self, request, **kwargs):
payment = self.get_object()
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id,
'provider': payment.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
return self.retrieve(request, [], **kwargs)
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderRefundSerializer
queryset = OrderRefund.objects.none()
permission = 'can_view_orders'
write_permission = 'can_change_orders'
lookup_field = 'local_id'
def get_queryset(self):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.refunds.all()
@detail_route(methods=['POST'])
def cancel(self, request, **kwargs):
refund = self.get_object()
if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_EXTERNAL):
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
refund.state = OrderRefund.REFUND_STATE_CANCELED
refund.save()
refund.order.log_action('pretix.event.order.refund.canceled', {
'local_id': refund.local_id,
'provider': refund.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def process(self, request, **kwargs):
refund = self.get_object()
if refund.state != OrderRefund.REFUND_STATE_EXTERNAL:
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
if request.data.get('mark_refunded', False):
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth)
else:
refund.order.status = Order.STATUS_PENDING
refund.order.set_expires(
now(),
refund.order.event.subevents.filter(
id__in=refund.order.positions.values_list('subevent_id', flat=True))
)
refund.order.save(update_fields=['status', 'expires'])
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def done(self, request, **kwargs):
refund = self.get_object()
if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT):
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
return self.retrieve(request, [], **kwargs)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return ctx
def create(self, request, *args, **kwargs):
mark_refunded = request.data.pop('mark_refunded', False)
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
r = serializer.instance
serializer = OrderRefundSerializer(r, context=serializer.context)
r.order.log_action(
'pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
},
user=request.user if request.user.is_authenticated else None,
auth=request.auth
)
if mark_refunded:
mark_order_refunded(
r.order,
user=request.user if request.user.is_authenticated else None,
auth=(request.auth if request.auth else None),
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
@@ -711,7 +396,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('nr',)
ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter
filter_class = InvoiceFilter
permission = 'can_view_orders'
lookup_url_kwarg = 'number'
lookup_field = 'nr'

View File

@@ -12,7 +12,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
lookup_url_kwarg = 'organizer'
def get_queryset(self):
if self.request.user.is_authenticated:
if self.request.user.is_authenticated():
if self.request.user.has_active_staff_session(self.request.session.session_key):
return Organizer.objects.all()
elif isinstance(self.request.auth, OAuthAccessToken):
@@ -23,7 +23,5 @@ 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

@@ -1,16 +0,0 @@
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
class MeView(APIView):
authentication_classes = (SessionAuthentication, OAuth2Authentication)
def get(self, request, format=None):
return Response({
'email': request.user.email,
'fullname': request.user.fullname,
'locale': request.user.locale,
'timezone': request.user.timezone
})

View File

@@ -1,16 +1,11 @@
import contextlib
from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from rest_framework import status, viewsets
from rest_framework.decorators import list_route
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
@@ -39,36 +34,15 @@ class VoucherViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('id',)
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filterset_class = VoucherFilter
filter_class = VoucherFilter
permission = 'can_view_vouchers'
write_permission = 'can_change_vouchers'
def get_queryset(self):
return self.request.event.vouchers.all()
def _predict_quota_check(self, data, instance):
# This method predicts if Voucher.clean_quota_needs_checking
# *migh* later require a quota check. It is only approximate
# and returns True a little too often. The point is to avoid
# locks when we know we won't need them.
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
return False
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
return False
if 'block_quota' in data and not data.get('block_quota'):
return False
if instance and 'block_quota' not in data and not instance.block_quota:
return False
return True
def create(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, None):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
with request.event.lock():
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
@@ -86,11 +60,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
return ctx
def update(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, self.get_object()):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
with request.event.lock():
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
@@ -112,24 +82,3 @@ class VoucherViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
super().perform_destroy(instance)
@list_route(methods=['POST'])
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i in serializer.instance:
i.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

View File

@@ -28,7 +28,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('created',)
ordering_fields = ('id', 'created', 'email', 'item')
filterset_class = WaitingListFilter
filter_class = WaitingListFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'

View File

@@ -1,49 +0,0 @@
from rest_framework import viewsets
from pretix.api.models import WebHook
from pretix.api.serializers.webhooks import WebHookSerializer
from pretix.helpers.dicts import merge_dicts
class WebHookViewSet(viewsets.ModelViewSet):
serializer_class = WebHookSerializer
queryset = WebHook.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
def get_queryset(self):
return self.request.organizer.webhooks.prefetch_related('listeners')
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.webhook.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
def perform_update(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.webhook.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
return inst
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.webhook.changed',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk, 'enabled': False}
)
instance.enabled = False
instance.save(update_fields=['enabled'])

View File

@@ -1,252 +0,0 @@
import json
import logging
import time
from collections import OrderedDict
import requests
from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from requests import RequestException
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
from pretix.api.signals import register_webhook_events
from pretix.base.models import LogEntry
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.celery_app import app
logger = logging.getLogger(__name__)
_ALL_EVENTS = None
class WebhookEvent:
def __init__(self):
pass
def __repr__(self):
return '<WebhookEvent: {}>'.format(self.action_type)
@property
def action_type(self) -> str:
"""
The action_type string that this notification handles, for example
``"pretix.event.order.paid"``. Only one notification type should be registered
per action type.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this notification type.
"""
raise NotImplementedError() # NOQA
def build_payload(self, logentry: LogEntry) -> dict:
"""
This is the main function that you should override. It is supposed to turn a log entry
object into a dictionary that can be used as the webhook payload.
"""
raise NotImplementedError() # NOQA
def get_all_webhook_events():
global _ALL_EVENTS
if _ALL_EVENTS:
return _ALL_EVENTS
types = OrderedDict()
for recv, ret in register_webhook_events.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.action_type] = r
else:
types[ret.action_type] = ret
_ALL_EVENTS = types
return types
class ParametrizedOrderWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
order = logentry.content_object
return {
'notification_id': logentry.pk,
'organizer': order.event.organizer.slug,
'event': order.event.slug,
'code': order.code,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
ParametrizedOrderWebhookEvent(
'pretix.event.order.placed',
_('New order placed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.paid',
_('Order marked as paid'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.canceled',
_('Order canceled'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.expired',
_('Order expired'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.modified',
_('Order information changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.contact.changed',
_('Order contact address changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.changed.*',
_('Order changed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.refund.created.externally',
_('External refund of payment'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.refunded',
_('Order refunded'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.approved',
_('Order approved'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.denied',
_('Order denied'),
),
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
),
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin.reverted',
_('Ticket check-in reverted'),
),
)
@app.task(base=TransactionAwareTask)
def notify_webhooks(logentry_id: int):
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.organizer:
return # We need to know the organizer
types = get_all_webhook_events()
notification_type = None
typepath = logentry.action_type
while not notification_type and '.' in typepath:
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if not notification_type:
return # Ignore, no webhooks for this event type
# All webhooks that registered for this notification
event_listener = WebHookEventListener.objects.filter(
webhook=OuterRef('pk'),
action_type=notification_type.action_type
)
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
organizer=logentry.organizer,
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
for wh in webhooks:
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
logentry = LogEntry.all.get(id=logentry_id)
webhook = WebHook.objects.get(id=webhook_id)
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
payload = event_type.build_payload(logentry)
t = time.time()
try:
try:
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError:
pass

View File

@@ -12,7 +12,6 @@ class PretixBaseConfig(AppConfig):
from . import exporters # NOQA
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
try:

View File

@@ -1,18 +1,7 @@
import logging
from smtplib import SMTPResponseException
from smtplib import SMTPRecipientsRefused, SMTPSenderRefused
import bleach
import markdown
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile
logger = logging.getLogger('pretix.base.email')
@@ -23,113 +12,15 @@ class CustomSMTPBackend(EmailBackend):
try:
self.open()
self.connection.ehlo_or_helo_if_needed()
self.connection.rcpt("test@example.org")
(code, resp) = self.connection.mail(from_addr, [])
if code != 250:
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
raise SMTPSenderRefused(code, resp, from_addr)
senderrs = {}
(code, resp) = self.connection.rcpt('test@example.com')
if (code != 250) and (code != 251):
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
raise SMTPRecipientsRefused(senderrs)
finally:
self.close()
class BaseHTMLMailRenderer:
"""
This is the base class for all HTML e-mail renderers.
"""
def __init__(self, event: Event):
self.event = event
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
"""
This method should generate the HTML part of the email.
:param plain_body: The body of the email in plain text.
:param plain_signature: The signature with event organizer contact details in plain text.
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:return: An HTML string
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
A human-readable name for this renderer. This should be short but self-explanatory.
"""
raise NotImplementedError() # NOQA
@property
def identifier(self) -> str:
"""
A short and unique identifier for this renderer.
This should only contain lowercase letters and in most cases will be the same as your package name or prefixed
with your package name.
"""
raise NotImplementedError() # NOQA
@property
def thumbnail_filename(self) -> str:
"""
A file name discoverable in the static file storage that contains a preview of your renderer. This should
be with aspect resolution 4:3.
"""
raise NotImplementedError() # NOQA
@property
def is_available(self) -> bool:
"""
This renderer will only be available if this returns ``True``. You can use this to limit this renderer
to certain events. Defaults to ``True``.
"""
return True
class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
@property
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
body_md = bleach.linkify(markdown_compile(plain_body))
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'body': body_md,
'subject': str(subject),
'color': '#8E44B3'
}
if self.event:
htmlctx['event'] = self.event
htmlctx['color'] = self.event.settings.primary_color
if plain_signature:
signature_md = plain_signature.replace('\n', '<br>\n')
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
htmlctx['signature'] = signature_md
if order:
htmlctx['order'] = order
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html
class ClassicMailRenderer(TemplateBasedMailRenderer):
verbose_name = _('pretix default')
identifier = 'classic'
thumbnail_filename = 'pretixbase/email/thumb.png'
template_name = 'pretixbase/email/plainwrapper.html'
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
def base_renderers(sender, **kwargs):
return [ClassicMailRenderer]

View File

@@ -5,12 +5,9 @@ from zipfile import ZipFile
import dateutil.parser
from django import forms
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import OrderPayment
from ..exporter import BaseExporter
from ..services.invoices import invoice_pdf_task
from ..signals import register_data_exporters
@@ -24,14 +21,7 @@ class InvoiceExporter(BaseExporter):
qs = self.event.invoices.filter(shredded=False)
if form_data.get('payment_provider'):
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
)
)
)
qs = qs.filter(has_payment_with_provider=1)
qs = qs.filter(order__payment_provider=form_data.get('payment_provider'))
if form_data.get('date_from'):
date_value = form_data.get('date_from')
@@ -94,10 +84,10 @@ class InvoiceExporter(BaseExporter):
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
],
required=False,
help_text=_('Only include invoices for orders that have at least one payment attempt '
'with this payment provider. '
'Note that this might include some invoices of orders which in the end have been '
'fully or partially paid with a different provider.')
help_text=_('Only include invoices for orders that are currently set to this payment provider. '
'Note that this might include some invoices of other payment providers or misses '
'some invoices if the payment provider of an order has been changed and a new invoice '
'has been generated.')
)),
]
)

View File

@@ -5,14 +5,13 @@ from decimal import Decimal
import pytz
from defusedcsv import csv
from django import forms
from django.db.models import DateTimeField, Max, OuterRef, Subquery, Sum
from django.db.models import Sum
from django.dispatch import receiver
from django.utils.formats import localize
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.models.orders import OrderFee
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -56,34 +55,15 @@ class OrderListExporter(BaseExporter):
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
payment_date__isnull=False
).values('order').annotate(
m=Max('payment_date')
).values(
'm'
).order_by()
qs = self.event.orders.annotate(
payment_date=Subquery(p_date, output_field=DateTimeField())
).select_related('invoice_address').prefetch_related('invoices')
qs = self.event.orders.all().select_related('invoice_address').prefetch_related('invoices')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Payment date'), _('Payment type'), _('Fees'), _('Order locale')
]
for tr in tax_rates:
@@ -97,6 +77,11 @@ class OrderListExporter(BaseExporter):
writer.writerow(headers)
provider_names = {
k: v.verbose_name
for k, v in self.event.get_payment_providers().items()
}
full_fee_sum_cache = {
o['order__id']: o['grosssum'] for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(grosssum=Sum('value'))
@@ -126,33 +111,25 @@ class OrderListExporter(BaseExporter):
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country_old,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += ['', '', '', '', '', '', '']
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
provider_names.get(order.payment_provider, order.payment_provider),
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00')),
order.locale,
]
for tr in tax_rates:
taxrate_values = sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
fee_taxrate_values = fee_sum_cache.get((order.id, tr),
{'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
fee_taxrate_values = fee_sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
row += [
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
@@ -167,77 +144,6 @@ class OrderListExporter(BaseExporter):
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
class PaymentListExporter(BaseExporter):
identifier = 'paymentlistcsv'
verbose_name = ugettext_lazy('List of payments and refunds (CSV)')
@property
def export_form_fields(self):
return OrderedDict(
[
('successful_only',
forms.BooleanField(
label=_('Only successful payments'),
initial=True,
required=False
)),
]
)
def render(self, form_data: dict):
output = io.StringIO()
tz = pytz.timezone(self.event.settings.timezone)
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
provider_names = {
k: v.verbose_name
for k, v in self.event.get_payment_providers().items()
}
payments = OrderPayment.objects.filter(
order__event=self.event,
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event
).order_by('created')
if form_data['successful_only']:
payments = payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
)
refunds = refunds.filter(
state=OrderRefund.REFUND_STATE_DONE,
)
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
headers = [
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Amount'), _('Payment method')
]
writer.writerow(headers)
for obj in objs:
if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date:
d2 = obj.execution_date.astimezone(tz).date().strftime('%Y-%m-%d')
else:
d2 = ''
row = [
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
d2,
obj.get_state_display(),
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
provider_names.get(obj.provider, obj.provider)
]
writer.writerow(row)
return '{}_payments.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
class QuotaListExporter(BaseExporter):
identifier = 'quotalistcsv'
verbose_name = ugettext_lazy('Quota availabilities (CSV)')
@@ -274,11 +180,6 @@ def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
def register_quotalist_exporter(sender, **kwargs):
return QuotaListExporter

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 k, f in self.fields.items():
for f in self.fields.values():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)

View File

@@ -39,7 +39,7 @@ class LoginForm(forms.Form):
password = self.cleaned_data.get('password')
if email and password:
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
self.user_cache = authenticate(email=email.lower(), password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
@@ -180,4 +180,12 @@ class PasswordForgotForm(forms.Form):
super().__init__(*args, **kwargs)
def clean_email(self):
return self.cleaned_data['email']
email = self.cleaned_data['email']
try:
self.cleaned_data['user'] = User.objects.get(email=email)
return email
except User.DoesNotExist:
raise forms.ValidationError(
_("We are unable to find a user matching the data you provided."),
code='unknown_user'
)

View File

@@ -1,4 +1,3 @@
import copy
import logging
from decimal import Decimal
@@ -9,7 +8,6 @@ import vat_moss.id
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.widgets import (
@@ -18,112 +16,12 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
class NamePartsWidget(forms.MultiWidget):
widget = forms.TextInput
def __init__(self, scheme: dict, field: forms.Field, attrs=None):
widgets = []
self.scheme = scheme
self.field = field
for fname, label, size in self.scheme['fields']:
a = copy.copy(attrs) or {}
a['data-fname'] = fname
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
def decompress(self, value):
if value is None:
return None
data = []
for i, field in enumerate(self.scheme['fields']):
fname, label, size = field
data.append(value.get(fname, ""))
if '_legacy' in value and not data[-1]:
data[-1] = value.get('_legacy', '')
return data
def render(self, name: str, value, attrs=None, renderer=None) -> str:
if not isinstance(value, list):
value = self.decompress(value)
output = []
final_attrs = self.build_attrs(attrs or dict())
if 'required' in final_attrs:
del final_attrs['required']
id_ = final_attrs.get('id', None)
for i, widget in enumerate(self.widgets):
try:
widget_value = value[i]
except (IndexError, TypeError):
widget_value = None
if id_:
final_attrs = dict(
final_attrs,
id='%s_%s' % (id_, i),
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
final_attrs['data-size'] = self.scheme['fields'][i][2]
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets) -> str:
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
class NamePartsFormField(forms.MultiValueField):
widget = NamePartsWidget
def compress(self, data_list) -> dict:
data = {}
data['_scheme'] = self.scheme_name
for i, value in enumerate(data_list):
data[self.scheme['fields'][i][0]] = value or ''
return data
def __init__(self, *args, **kwargs):
fields = []
defaults = {
'widget': self.widget,
'max_length': kwargs.pop('max_length', None),
}
self.scheme_name = kwargs.pop('scheme')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
self.one_required = kwargs.get('required', True)
require_all_fields = kwargs.pop('require_all_fields', False)
kwargs['required'] = False
kwargs['widget'] = (kwargs.get('widget') or self.widget)(
scheme=self.scheme, field=self, **kwargs.pop('widget_kwargs', {})
)
defaults.update(**kwargs)
for fname, label, size in self.scheme['fields']:
defaults['label'] = label
field = forms.CharField(**defaults)
field.part_name = fname
fields.append(field)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
self.require_all_fields = require_all_fields
self.required = self.one_required
def clean(self, value) -> dict:
value = super().clean(value)
if self.one_required and (not value or not any(v for v in value)):
raise forms.ValidationError(self.error_messages['required'], code='required')
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
return value
class BaseQuestionsForm(forms.Form):
"""
This form class is responsible for asking order-related questions. This includes
@@ -148,12 +46,10 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs)
if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required,
scheme=event.settings.name_scheme,
self.fields['attendee_name'] = forms.CharField(
max_length=255, required=event.settings.attendee_names_required,
label=_('Attendee name'),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name),
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
@@ -170,7 +66,6 @@ class BaseQuestionsForm(forms.Form):
else:
initial = None
tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text)
if q.type == Question.TYPE_BOOLEAN:
if q.required:
# For some reason, django-bootstrap3 does not set the required attribute
@@ -186,7 +81,7 @@ class BaseQuestionsForm(forms.Form):
field = forms.BooleanField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=initialbool, widget=widget,
)
elif q.type == Question.TYPE_NUMBER:
@@ -199,13 +94,13 @@ class BaseQuestionsForm(forms.Form):
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_TEXT:
field = forms.CharField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
widget=forms.Textarea,
initial=initial.answer if initial else None,
)
@@ -213,7 +108,7 @@ class BaseQuestionsForm(forms.Form):
field = forms.ModelChoiceField(
queryset=q.options,
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
widget=forms.Select,
empty_label='',
initial=initial.options.first() if initial else None,
@@ -222,35 +117,35 @@ class BaseQuestionsForm(forms.Form):
field = forms.ModelMultipleChoiceField(
queryset=q.options,
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
widget=forms.CheckboxSelectMultiple,
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
field = forms.FileField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
widget=DatePickerWidget(),
)
elif q.type == Question.TYPE_TIME:
field = forms.TimeField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
field = SplitDateTimeField(
field = forms.SplitDateTimeField(
label=q.question, required=q.required,
help_text=help_text,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
@@ -274,12 +169,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'name': forms.TextInput(attrs={}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -294,51 +190,27 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
if not event.settings.invoice_address_required:
for k, f in self.fields.items():
f.required = False
f.widget.is_required = False
if 'required' in f.widget.attrs:
del f.widget.attrs['required']
elif event.settings.invoice_address_company_required:
self.initial['is_business'] = True
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
self.fields['company'].required = True
self.fields['company'].widget.is_required = True
self.fields['company'].widget.attrs['required'] = 'required'
del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency']
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.invoice_name_required,
scheme=event.settings.name_scheme,
label=_('Name'),
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required:
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
if event.settings.invoice_name_required:
self.fields['name'].required = True
else:
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
def clean(self):
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
if not data.get('is_business') and not data.get('name_parts'):
raise ValidationError(_('You need to provide your name.'))
if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required:
raise ValidationError(_('You need to provide either a company name or your name.'))
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
self.instance.name_parts = data.get('name_parts')
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
@@ -350,7 +222,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
country_code, normalized_id, company_name = result
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except (vat_moss.errors.InvalidError, ValueError):
except vat_moss.errors.InvalidError:
raise ValidationError(_('This VAT ID is not valid. Please re-check your input.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))

View File

@@ -2,7 +2,6 @@ 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 _
@@ -93,20 +92,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
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)
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)
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),
@@ -117,22 +110,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
class BusinessBooleanRadio(forms.RadioSelect):
def __init__(self, require_business=False, attrs=None):
self.require_business = require_business
if self.require_business:
choices = (
('business', _('Business customer')),
)
else:
choices = (
('individual', _('Individual customer')),
('business', _('Business customer')),
)
def __init__(self, attrs=None):
choices = (
('individual', _('Individual customer')),
('business', _('Business customer')),
)
super().__init__(attrs, choices)
def format_value(self, value):
if self.require_business:
return 'business'
try:
return {True: 'business', False: 'individual'}[value]
except KeyError:
@@ -140,8 +125,6 @@ class BusinessBooleanRadio(forms.RadioSelect):
def value_from_datadict(self, data, files, name):
value = data.get(name)
if self.require_business:
return True
return {
'business': True,
True: True,

View File

@@ -184,22 +184,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
class ThumbnailingImageReader(ImageReader):
def resize(self, width, height, dpi):
if width is None:
width = height * self._image.size[0] / self._image.size[1]
if height is None:
height = width * self._image.size[1] / self._image.size[0]
self._image.thumbnail(
size=(int(width * dpi / 72), int(height * dpi / 72)),
resample=BICUBIC
)
self._data = None
return width, height
def _jpeg_fh(self):
# Bypass a reportlab-internal optimization that falls back to the original
# file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image.
return None
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
@@ -216,18 +204,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.restoreState()
def _draw_invoice_to(self, canvas):
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
def _draw_invoice_from(self, canvas):
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
def _on_first_page(self, canvas: Canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
@@ -244,14 +220,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
self._draw_invoice_from(canvas)
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
self._draw_invoice_to(canvas)
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
@@ -330,7 +312,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return txt
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
if self.invoice.event.settings.show_date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
@@ -386,7 +368,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
pgettext('invoice', 'Your reference: {reference}').format(reference=self.invoice.internal_reference),
self.stylesheet['Normal']
))

View File

@@ -8,10 +8,10 @@ class Command(BaseCommand):
help = "Rebuild static files and language files"
def handle(self, *args, **options):
call_command('compilemessages', verbosity=1)
call_command('compilejsi18n', verbosity=1)
call_command('compilemessages', verbosity=1, interactive=False)
call_command('compilejsi18n', verbosity=1, interactive=False)
call_command('collectstatic', verbosity=1, interactive=False)
call_command('compress', verbosity=1)
call_command('compress', verbosity=1, interactive=False)
try:
gs = GlobalSettingsObject()
del gs.settings.update_check_last

View File

@@ -3,8 +3,8 @@ from urllib.parse import urlsplit
import pytz
from django.conf import settings
from django.core.urlresolvers import get_script_prefix
from django.http import HttpRequest, HttpResponse
from django.urls import get_script_prefix
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin

View File

@@ -28,8 +28,7 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
db_index=True)),
('email', models.EmailField(max_length=254, blank=True, unique=True, verbose_name='E-mail', null=True, db_index=True)),
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
('is_active', models.BooleanField(verbose_name='Is active', default=True)),

View File

@@ -1,424 +0,0 @@
# Generated by Django 2.0.8 on 2018-09-11 14:50
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.db.models import F
from django.db.models.functions import Concat
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext as _
import pretix.base.models.auth
import pretix.base.validators
from pretix.base.i18n import language
def create_checkin_lists(apps, schema_editor):
Event = apps.get_model('pretixbase', 'Event')
Checkin = apps.get_model('pretixbase', 'Checkin')
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
for e in Event.objects.all():
locale = EventSettingsStore.objects.filter(object=e, key='locale').first()
if locale:
locale = locale.value
else:
locale = settings.LANGUAGE_CODE
if e.has_subevents:
for se in e.subevents.all():
with language(locale):
cl = e.checkin_lists.create(name=se.name, subevent=se, all_products=True)
Checkin.objects.filter(position__subevent=se, position__order__event=e).update(list=cl)
else:
with language(locale):
cl = e.checkin_lists.create(name=_('Default list'), all_products=True)
Checkin.objects.filter(position__order__event=e).update(list=cl)
def set_full_invoice_no(app, schema_editor):
Invoice = app.get_model('pretixbase', 'Invoice')
Invoice.objects.all().update(
full_invoice_no=Concat(F('prefix'), F('invoice_no'))
)
def set_position(apps, schema_editor):
Question = apps.get_model('pretixbase', 'Question')
for q in Question.objects.all():
for i, option in enumerate(q.options.all()):
option.position = i
option.save()
def set_is_staff(apps, schema_editor):
User = apps.get_model('pretixbase', 'User')
User.objects.filter(is_superuser=True).update(is_staff=True)
def set_identifiers(apps, schema_editor):
Question = apps.get_model('pretixbase', 'Question')
QuestionOption = apps.get_model('pretixbase', 'QuestionOption')
for q in Question.objects.select_related('event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not Question.objects.filter(event=q.event, identifier=code).exists():
q.identifier = code
q.save()
break
for q in QuestionOption.objects.select_related('question', 'question__event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not QuestionOption.objects.filter(question__event=q.question.event, identifier=code).exists():
q.identifier = code
q.save()
break
class Migration(migrations.Migration):
replaces = [('pretixbase', '0077_auto_20171124_1629'), ('pretixbase', '0078_auto_20171206_1603'),
('pretixbase', '0079_auto_20180115_0855'), ('pretixbase', '0080_question_ask_during_checkin'),
('pretixbase', '0081_auto_20180220_1031'), ('pretixbase', '0082_auto_20180222_0938'),
('pretixbase', '0083_auto_20180228_2102'), ('pretixbase', '0084_questionoption_position'),
('pretixbase', '0085_auto_20180312_1119'), ('pretixbase', '0086_auto_20180320_1219'),
('pretixbase', '0087_auto_20180317_1952'), ('pretixbase', '0088_auto_20180328_1217')]
dependencies = [
('pretixbase', '0076_orderfee_squashed_0082_invoiceaddress_internal_reference'),
]
operations = [
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be '
'unique among your events. We recommend some kind of abbreviation or a date with less than '
'10 characters that can be easily remembered, but you can also choose to use a random '
'value. This will be used in URLs, order codes, invoice numbers, and bank transfer '
'references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
name='name',
field=models.CharField(db_index=True,
help_text='Can not contain spaces or special characters except underscores',
max_length=50, validators=[django.core.validators.RegexValidator(
message='The property name may only contain letters, numbers and underscores.',
regex='^[a-zA-Z0-9_]+$')], verbose_name='Name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can '
'only be used once. This is being used in URLs to refer to your organizer accounts and your'
' events.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('all_products',
models.BooleanField(default=True, verbose_name='All products (including newly created ones)')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkin_lists',
to='pretixbase.Event')),
('subevent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date')),
('limit_products',
models.ManyToManyField(blank=True, to='pretixbase.Item', verbose_name='Limit to products')),
],
),
migrations.AddField(
model_name='checkin',
name='list',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='checkins', to='pretixbase.CheckinList'),
),
migrations.RunPython(
code=create_checkin_lists,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='checkin',
name='list',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='checkins',
to='pretixbase.CheckinList'),
),
migrations.CreateModel(
name='NotificationSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('enabled', models.BooleanField(default=True)),
],
),
migrations.AlterUniqueTogether(
name='notificationsetting',
unique_together={('user', 'action_type', 'event', 'method')},
),
migrations.AddField(
model_name='logentry',
name='visible',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='notificationsetting',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='notification_settings', to='pretixbase.Event'),
),
migrations.AlterField(
model_name='notificationsetting',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings',
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='user',
name='notifications_send',
field=models.BooleanField(default=True, help_text='If turned off, you will not get any notifications.',
verbose_name='Receive notifications according to my settings below'),
),
migrations.AddField(
model_name='user',
name='notifications_token',
field=models.CharField(default=pretix.base.models.auth.generate_notifications_token, max_length=255),
),
migrations.AddField(
model_name='invoice',
name='full_invoice_no',
field=models.CharField(db_index=True, default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(
choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'),
('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload'),
('D', 'Date'), ('H', 'Time'), ('W', 'Date and time')], max_length=5,
verbose_name='Question type'),
),
migrations.RunPython(
code=set_full_invoice_no,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AddField(
model_name='question',
name='ask_during_checkin',
field=models.BooleanField(default=False,
help_text='This will only work if you handle your check-in with pretixdroid 1.8 '
'or '
'newer or pretixdesk 0.2 or newer.',
verbose_name='Ask during check-in instead of in the ticket buying process'),
),
migrations.AddField(
model_name='checkinlist',
name='include_pending',
field=models.BooleanField(default=False,
help_text='With this option, people will be able to check in even if the order '
'have '
'not been paid. This only works with pretixdesk 0.3.0 or newer or '
'pretixdroid 1.9 or newer.',
verbose_name='Include pending orders'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True,
help_text='Optional. No products will be sold after this date. If you do not '
'set '
'this value, the presale will end after the end date of your event.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='logentry',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='pretixbase.Event'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True,
help_text='Optional. No products will be sold after this date. If you do not '
'set '
'this value, the presale will end after the end date of your event.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='user',
name='require_2fa',
field=models.BooleanField(default=False, verbose_name='Two-factor authentification is required to log in'),
),
migrations.AddField(
model_name='order',
name='checkin_attention',
field=models.BooleanField(default=False,
help_text='If you set this, the check-in app will show a visible warning that '
'tickets of this order require special attention. This will not show '
'any '
'details or custom message, so you need to brief your check-in staff '
'how '
'to handle these cases.',
verbose_name='Requires special attention'),
),
migrations.AddField(
model_name='taxrule',
name='custom_rules',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(
choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'),
('other', 'Other fees')], max_length=100),
),
migrations.AlterModelOptions(
name='questionoption',
options={'ordering': ('position', 'id'), 'verbose_name': 'Question option',
'verbose_name_plural': 'Question options'},
),
migrations.AddField(
model_name='questionoption',
name='position',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='question',
name='position',
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
),
migrations.RunPython(
code=set_position,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AddField(
model_name='question',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AddField(
model_name='questionoption',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(
choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'),
('da', 'Danish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50,
verbose_name='Language'),
),
migrations.RunPython(
code=set_identifiers,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='cachedcombinedticket',
name='file',
field=models.FileField(blank=True, max_length=255, null=True,
upload_to=pretix.base.models.orders.cachedcombinedticket_name),
),
migrations.AlterField(
model_name='cachedticket',
name='file',
field=models.FileField(blank=True, max_length=255, null=True,
upload_to=pretix.base.models.orders.cachedticket_name),
),
migrations.AlterField(
model_name='invoice',
name='file',
field=models.FileField(blank=True, max_length=255, null=True,
upload_to=pretix.base.models.invoices.invoice_filename),
),
migrations.AlterField(
model_name='question',
name='identifier',
field=models.CharField(
help_text='You can enter any value here to make it easier to match the data with other sources. If '
'you do '
'not input one, we will generate one automatically.',
max_length=190, verbose_name='Internal identifier'),
),
migrations.AlterField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, max_length=255, null=True,
upload_to=pretix.base.models.orders.answerfile_name),
),
migrations.RunPython(
code=set_is_staff,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='user',
name='is_superuser',
),
migrations.CreateModel(
name='StaffSession',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_start', models.DateTimeField(auto_now_add=True)),
('date_end', models.DateTimeField(blank=True, null=True)),
('session_key', models.CharField(max_length=255)),
('comment', models.TextField()),
],
),
migrations.CreateModel(
name='StaffSessionAuditLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('url', models.CharField(max_length=255)),
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs',
to='pretixbase.StaffSession')),
('impersonating', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL)),
('method', models.CharField(default='GET', max_length=255)),
],
options={
'ordering': ('datetime',),
},
),
migrations.AddField(
model_name='staffsession',
name='user',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AlterModelOptions(
name='staffsession',
options={'ordering': ('date_start',)},
),
migrations.AlterField(
model_name='item',
name='picture',
field=models.ImageField(blank=True, max_length=255, null=True,
upload_to=pretix.base.models.items.itempicture_upload_to,
verbose_name='Product picture'),
),
]

View File

@@ -9,7 +9,6 @@ class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0088_auto_20180328_1217'),
('pretixapi', '0001_initial')
]
operations = [

View File

@@ -1,85 +0,0 @@
# Generated by Django 2.0.8 on 2018-09-11 14:54
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils.crypto import get_random_string
def set_pids(apps, schema_editor):
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
taken = set()
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
for op in OrderPosition.objects.iterator():
while True:
code = get_random_string(length=10, allowed_chars=charset)
if code not in taken:
op.pseudonymization_id = code
taken.add(code)
break
op.save(update_fields=['pseudonymization_id'])
class Migration(migrations.Migration):
replaces = [('pretixbase', '0090_auto_20180509_0917'), ('pretixbase', '0091_auto_20180513_1641'),
('pretixbase', '0092_auto_20180511_1224'), ('pretixbase', '0093_auto_20180528_1432'),
('pretixbase', '0094_auto_20180604_1119'), ('pretixbase', '0095_auto_20180604_1129')]
dependencies = [
('pretixbase', '0089_auto_20180315_1322'),
('pretixapi', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='item',
name='internal_name',
field=models.CharField(blank=True,
help_text='If you set this, this will be used instead of the public name in the '
'backend.',
max_length=255, null=True, verbose_name='Internal name'),
),
migrations.AddField(
model_name='itemcategory',
name='internal_name',
field=models.CharField(blank=True,
help_text='If you set this, this will be used instead of the public name in the '
'backend.',
max_length=255, null=True, verbose_name='Internal name'),
),
migrations.AddField(
model_name='order',
name='last_modified',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AddField(
model_name='item',
name='original_price',
field=models.DecimalField(blank=True, decimal_places=2,
help_text='If set, this will be displayed next to the current price to show '
'that the current price is a discounted one. This is just a cosmetic '
'setting and will not actually impact pricing.',
max_digits=7, null=True, verbose_name='Original price'),
),
migrations.AddField(
model_name='orderposition',
name='pseudonymization_id',
field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
),
migrations.RunPython(
code=set_pids,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='orderposition',
name='pseudonymization_id',
field=models.CharField(db_index=True, default='', max_length=16, unique=True),
preserve_default=False,
),
migrations.AddField(
model_name='logentry',
name='oauth_application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
),
]

View File

@@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-07-22 08:01
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0095_auto_20180604_1129'),
]
operations = [
migrations.CreateModel(
name='OrderPayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('local_id', models.PositiveIntegerField()),
('state', models.CharField(choices=[('created', 'created'), ('pending', 'pending'), ('confirmed', 'confirmed'), ('canceled', 'canceled'), ('failed', 'failed'), ('refunded', 'refunded')], max_length=190)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
('created', models.DateTimeField(auto_now_add=True)),
('payment_date', models.DateTimeField(blank=True, null=True)),
('provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
('info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
('migrated', models.BooleanField(default=False)),
],
options={
'ordering': ('local_id',),
},
),
migrations.CreateModel(
name='OrderRefund',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('local_id', models.PositiveIntegerField()),
('state', models.CharField(choices=[('external', 'started externally'), ('created', 'created'), ('transit', 'in transit'), ('done', 'done'), ('failed', 'failed'), ('canceled', 'canceled')], max_length=190)),
('source', models.CharField(choices=[('admin', 'Organizer'), ('buyer', 'Customer'), ('external', 'External')], max_length=190)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
('created', models.DateTimeField(auto_now_add=True)),
('execution_date', models.DateTimeField(blank=True, null=True)),
('provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
('info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refunds', to='pretixbase.Order', verbose_name='Order')),
('payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='refunds', to='pretixbase.OrderPayment')),
],
options={
'ordering': ('local_id',),
},
),
migrations.AlterModelOptions(
name='quota',
options={'ordering': ('name',), 'verbose_name': 'Quota', 'verbose_name_plural': 'Quotas'},
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('other', 'Other fees'), ('giftcard', 'Gift card')], max_length=100),
),
migrations.AlterField(
model_name='team',
name='can_change_organizer_settings',
field=models.BooleanField(default=False, help_text='Someone with this setting can get access to most data of all of your events, i.e. via privacy reports, so be careful who you add to this team!', verbose_name='Can change organizer settings'),
),
migrations.AlterField(
model_name='user',
name='require_2fa',
field=models.BooleanField(default=False, verbose_name='Two-factor authentication is required to log in'),
),
migrations.AddField(
model_name='orderpayment',
name='fee',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='pretixbase.OrderFee'),
),
migrations.AddField(
model_name='orderpayment',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payments', to='pretixbase.Order', verbose_name='Order'),
),
]

View File

@@ -1,118 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-07-22 08:04
from __future__ import unicode_literals
from django.db import migrations
def create_payments(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order') # noqa
OrderPayment = apps.get_model('pretixbase', 'OrderPayment') # noqa
OrderRefund = apps.get_model('pretixbase', 'OrderRefund') # noqa
payments = []
refunds = []
for o in Order.objects.filter(payments__isnull=True).iterator():
if o.status == 'n' or o.status == 'e':
payments.append(OrderPayment(
local_id=1,
state='created',
amount=o.total,
order=o,
provider=o.payment_provider,
info=o.payment_info,
migrated=True,
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
))
pass
elif o.status == 'p':
payments.append(OrderPayment(
local_id=1,
state='confirmed',
amount=o.total,
order=o,
provider=o.payment_provider,
payment_date=o.payment_date,
info=o.payment_info,
migrated=True,
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
))
elif o.status == 'r':
p = OrderPayment.objects.create(
local_id=1,
state='refunded',
amount=o.total,
order=o,
provider=o.payment_provider,
payment_date=o.payment_date,
info=o.payment_info,
migrated=True,
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
)
refunds.append(OrderRefund(
local_id=1,
state='done',
amount=o.total,
order=o,
provider=o.payment_provider,
info=o.payment_info,
source='admin',
payment=p
))
elif o.status == 'c':
payments.append(OrderPayment(
local_id=1,
state='canceled',
amount=o.total,
order=o,
provider=o.payment_provider,
payment_date=o.payment_date,
info=o.payment_info,
migrated=True,
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
))
if len(payments) > 500:
OrderPayment.objects.bulk_create(payments)
payments.clear()
if len(refunds) > 500:
OrderRefund.objects.bulk_create(refunds)
refunds.clear()
if len(payments) > 0:
OrderPayment.objects.bulk_create(payments)
if len(refunds) > 0:
OrderRefund.objects.bulk_create(refunds)
def notifications(apps, schema_editor):
NotificationSetting = apps.get_model('pretixbase', 'NotificationSetting')
for n in NotificationSetting.objects.filter(action_type='pretix.event.action_required'):
n.pk = None
n.action_type = 'pretix.event.order.refund.created.externally'
n.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0096_auto_20180722_0801'),
]
operations = [
migrations.RunPython(create_payments, migrations.RunPython.noop),
migrations.RunPython(notifications, migrations.RunPython.noop),
migrations.RemoveField(
model_name='order',
name='payment_date',
),
migrations.RemoveField(
model_name='order',
name='payment_info',
),
migrations.RemoveField(
model_name='order',
name='payment_manual',
),
migrations.RemoveField(
model_name='order',
name='payment_provider',
),
]

View File

@@ -1,56 +0,0 @@
# Generated by Django 2.0.7 on 2018-07-31 12:43
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.validators
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0097_auto_20180722_0804'),
]
operations = [
migrations.AlterModelOptions(
name='logentry',
options={'ordering': ('-datetime', '-id')},
),
migrations.AlterField(
model_name='orderpayment',
name='fee',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='pretixbase.OrderFee'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='impersonating',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='session',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='pretixbase.StaffSession'),
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('tr', 'Turkish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
),
migrations.AlterUniqueTogether(
name='event',
unique_together={('organizer', 'slug')},
),
]

View File

@@ -1,82 +0,0 @@
# Generated by Django 2.0.8 on 2018-09-11 14:54
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.validators
class Migration(migrations.Migration):
replaces = [('pretixbase', '0098_auto_20180731_1243'), ('pretixbase', '0099_auto_20180807_0841'), ('pretixbase', '0100_item_require_approval')]
dependencies = [
('pretixbase', '0097_auto_20180722_0804'),
]
operations = [
migrations.AlterModelOptions(
name='logentry',
options={'ordering': ('-datetime', '-id')},
),
migrations.AlterField(
model_name='orderpayment',
name='fee',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='pretixbase.OrderFee'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='impersonating',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='session',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='pretixbase.StaffSession'),
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('tr', 'Turkish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
),
migrations.AlterUniqueTogether(
name='event',
unique_together={('organizer', 'slug')},
),
migrations.AlterModelOptions(
name='waitinglistentry',
options={'ordering': ('-priority', 'created'), 'verbose_name': 'Waiting list entry', 'verbose_name_plural': 'Waiting list entries'},
),
migrations.AddField(
model_name='waitinglistentry',
name='priority',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='waitinglistentry',
name='voucher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Voucher', verbose_name='Assigned voucher'),
),
migrations.AddField(
model_name='item',
name='require_approval',
field=models.BooleanField(default=False, help_text='If this product is part of an order, the order will be put into an "approval" state and will need to be confirmed by you before it can be paid and completed. You can use this e.g. for discounted tickets that are only available to specific groups.', verbose_name='Buying this product requires approval.'),
),
migrations.AddField(
model_name='order',
name='require_approval',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 2.1 on 2018-08-07 08:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0098_auto_20180731_1243'),
]
operations = [
migrations.AlterModelOptions(
name='waitinglistentry',
options={'ordering': ('-priority', 'created'), 'verbose_name': 'Waiting list entry', 'verbose_name_plural': 'Waiting list entries'},
),
migrations.AddField(
model_name='waitinglistentry',
name='priority',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='waitinglistentry',
name='voucher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Voucher', verbose_name='Assigned voucher'),
),
]

View File

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

@@ -1,79 +0,0 @@
# Generated by Django 2.1 on 2018-10-23 23:00
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0099_auto_20180912_1035'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_from_city',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_tax_id',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_vat_id',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_from_zipcode',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_city',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_company',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_name',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_vat_id',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoice',
name='invoice_to_zipcode',
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 2.1 on 2018-08-09 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0099_auto_20180807_0841'),
]
operations = [
migrations.AddField(
model_name='item',
name='require_approval',
field=models.BooleanField(default=False, help_text='If this product is part of an order, the order will be put into an "approval" state and will need to be confirmed by you before it can be paid and completed. You can use this e.g. for discounted tickets that are only available to specific groups.', verbose_name='Buying this product requires approval.'),
),
migrations.AddField(
model_name='order',
name='require_approval',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.1 on 2018-10-25 22:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0100_auto_20181023_2300'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='reverse_charge',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,96 +0,0 @@
# Generated by Django 2.1 on 2018-10-17 00:24
import jsonfallback.fields
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations
from django_mysql.checks import mysql_connections
from django_mysql.utils import connection_is_mariadb
def set_attendee_name_parts(apps, schema_editor):
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
for op in OrderPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
op.save(update_fields=['attendee_name_parts'])
CartPosition = apps.get_model('pretixbase', 'CartPosition') # noqa
for op in CartPosition.objects.exclude(attendee_name_cached=None).exclude(
attendee_name_cached__isnull=True).iterator():
op.attendee_name_parts = {'_legacy': op.attendee_name_cached}
op.save(update_fields=['attendee_name_parts'])
InvoiceAddress = apps.get_model('pretixbase', 'InvoiceAddress') # noqa
for ia in InvoiceAddress.objects.exclude(name_cached=None).exclude(
name_cached__isnull=True).iterator():
ia.name_parts = {'_legacy': ia.name_cached}
ia.save(update_fields=['name_parts'])
def check_mysqlversion(apps, schema_editor):
errors = []
any_conn_works = False
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if connection_is_mariadb(conn) and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:
found = 'MariaDB ' + '.'.join(str(v) for v in conn.mysql_version)
elif hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (5, 7):
any_conn_works = True
else:
found = 'MySQL ' + '.'.join(str(v) for v in conn.mysql_version)
if conns and not any_conn_works:
raise ImproperlyConfigured(
'As of pretix 2.2, you need MySQL 5.7+ or MariaDB 10.2.7+ to run pretix. However, we detected a '
'database connection to {}'.format(found)
)
return errors
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0101_auto_20181025_2255'),
]
operations = [
migrations.RunPython(
check_mysqlversion, migrations.RunPython.noop
),
migrations.RenameField(
model_name='cartposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='orderposition',
old_name='attendee_name',
new_name='attendee_name_cached',
),
migrations.RenameField(
model_name='invoiceaddress',
old_name='name',
new_name='name_cached',
),
migrations.AddField(
model_name='cartposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
preserve_default=False,
),
migrations.RunPython(set_attendee_name_parts, migrations.RunPython.noop)
]

View File

@@ -2,7 +2,6 @@ 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,
@@ -16,9 +15,9 @@ from .log import LogEntry
from .notifications import NotificationSetting
from .orders import (
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
QuestionAnswer, cachedcombinedticket_name, cachedticket_name,
generate_position_secret, generate_secret,
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,

View File

@@ -75,7 +75,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = []
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('E-mail'), max_length=190)
verbose_name=_('E-mail'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -340,7 +340,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT)
user = models.ForeignKey('User')
date_start = models.DateTimeField(auto_now_add=True)
date_end = models.DateTimeField(null=True, blank=True)
session_key = models.CharField(max_length=255)
@@ -351,11 +351,11 @@ class StaffSession(models.Model):
class StaffSessionAuditLog(models.Model):
session = models.ForeignKey('StaffSession', related_name='logs', on_delete=models.PROTECT)
session = models.ForeignKey('StaffSession', related_name='logs')
datetime = models.DateTimeField(auto_now_add=True)
url = models.CharField(max_length=255)
method = models.CharField(max_length=255)
impersonating = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
impersonating = models.ForeignKey('User', null=True, blank=True)
class Meta:
ordering = ('datetime',)

View File

@@ -3,7 +3,6 @@ import uuid
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.crypto import get_random_string
@@ -48,12 +47,10 @@ 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
from ..services.notifications import notify
from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
event = None
if isinstance(self, Event):
@@ -70,8 +67,6 @@ 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
@@ -81,21 +76,8 @@ class LoggingMixin:
if save:
logentry.save()
no_types = get_all_notification_types()
wh_types = get_all_webhook_events()
no_type = None
wh_type = None
typepath = logentry.action_type
while (not no_type or not wh_types) and '.' in typepath:
wh_type = wh_type or wh_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
no_type = no_type or no_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if no_type:
if action in get_all_notification_types():
notify.apply_async(args=(logentry.pk,))
if wh_type:
notify_webhooks.apply_async(args=(logentry.pk,))
return logentry
@@ -114,50 +96,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', 'device')
class LockModel:
def refresh_for_update(self, fields=None, using=None, **kwargs):
"""
Like refresh_from_db(), but with select_for_update().
See also https://code.djangoproject.com/ticket/28344
"""
if fields is not None:
if not fields:
return
if any(LOOKUP_SEP in f for f in fields):
raise ValueError(
'Found "%s" in fields argument. Relations and transforms '
'are not allowed in fields.' % LOOKUP_SEP)
hints = {'instance': self}
db_instance_qs = self.__class__._base_manager.db_manager(using, hints=hints).filter(pk=self.pk).select_for_update(**kwargs)
# Use provided fields, if not set then reload all non-deferred fields.
deferred_fields = self.get_deferred_fields()
if fields is not None:
fields = list(fields)
db_instance_qs = db_instance_qs.only(*fields)
elif deferred_fields:
fields = [f.attname for f in self._meta.concrete_fields
if f.attname not in deferred_fields]
db_instance_qs = db_instance_qs.only(*fields)
db_instance = db_instance_qs.get()
non_loaded_fields = db_instance.get_deferred_fields()
for field in self._meta.concrete_fields:
if field.attname in non_loaded_fields:
# This field wasn't refreshed - skip ahead.
continue
setattr(self, field.attname, getattr(db_instance, field.attname))
# Clear cached foreign keys.
if field.is_relation and field.is_cached(self):
field.delete_cached_value(self)
# Clear cached relations.
for field in self._meta.related_objects:
if field.is_cached(self):
field.delete_cached_value(self)
self._state.db = db_instance._state.db
).select_related('user', 'event', 'oauth_application', 'api_token')

View File

@@ -8,12 +8,12 @@ from pretix.base.models import LoggedModel
class CheckinList(LoggedModel):
event = models.ForeignKey('Event', related_name='checkin_lists', on_delete=models.CASCADE)
event = models.ForeignKey('Event', related_name='checkin_lists')
name = models.CharField(max_length=190)
all_products = models.BooleanField(default=True, verbose_name=_("All products (including newly created ones)"))
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'), on_delete=models.CASCADE)
verbose_name=pgettext_lazy('subevent', 'Date'))
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
@@ -157,7 +157,7 @@ class Checkin(models.Model):
"""
A check-in object is created when a person enters the event.
"""
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins', on_delete=models.CASCADE)
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
list = models.ForeignKey(

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