Compare commits

..

11 Commits

Author SHA1 Message Date
Raphael Michel
8e20b07610 Fix MySQL package 2018-05-28 10:12:47 +02:00
Raphael Michel
ffeb1e1746 Bump version to 1.15.2 2018-05-28 10:06:18 +02:00
Felix Rindt
4692b3e9d2 Fix bug in date/time question stats (#916)
Fix bug in date/time question stats
2018-05-28 10:05:30 +02:00
Raphael Michel
98ea9ae91c Fix setup.py 2018-05-28 09:59:59 +02:00
Raphael Michel
40f401b549 Upgrade kombu 2018-05-28 09:59:59 +02:00
Raphael Michel
37e279bf27 Pin celery version 2018-05-28 09:59:59 +02:00
Raphael Michel
92addff5d3 Bump version to 1.15.1 2018-05-11 14:50:44 +02:00
Felix Rindt
585578edf2 Add subevent bulk create button when no exist (#904) 2018-05-11 14:46:06 +02:00
Raphael Michel
12271047e6 Fix #903 -- Incorrect price calculation for variations 2018-05-11 14:46:06 +02:00
robbi5
8413269c32 Add badge plugin support to MANIFEST.in (#902) 2018-05-11 14:46:06 +02:00
Raphael Michel
1f01ef6e37 Do not show end time if not set 2018-05-11 14:46:06 +02:00
481 changed files with 32455 additions and 86731 deletions

View File

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

View File

@@ -15,17 +15,17 @@ if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
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
if [ "$1" == "doc-spelling" ]; then
if [ "$1" == "spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
cd doc
make spelling
@@ -33,27 +33,22 @@ if [ "$1" == "doc-spelling" ]; then
exit 1
fi
fi
if [ "$1" == "translation-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
cd src
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
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 2 tests
py.test --reruns 5 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

@@ -15,28 +15,34 @@ matrix:
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
env: JOB=tests-cov
- python: 3.6
env: JOB=style
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
- python: 3.6
env: JOB=doc-spelling
- python: 3.6
env: JOB=translation-spelling
env: JOB=spelling
addons:
postgresql: "9.4"
apt:
packages:
- enchant
- myspell-de-de
- aspell-en
branches:
except:
- /^weblate-.*/

View File

@@ -3,7 +3,7 @@ cd /pretix/src
export DJANGO_SETTINGS_MODULE=production_settings
export DATA_DIR=/data/
export HOME=/pretix
export NUM_WORKERS=$((2 * $(nproc --all)))
NUM_WORKERS=10
if [ ! -d /data/logs ]; then
mkdir /data/logs;

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

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

@@ -6,13 +6,27 @@ with pretix' REST API, such as authentication, pagination and similar definition
.. _`rest-auth`:
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
Authentication
--------------
If you're building an application for end users, we strongly recommend that you use our
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
can also go with static API tokens that you can create on a per-team basis (see below).
You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:
@@ -30,24 +44,6 @@ like the following:
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
-----------
@@ -113,41 +109,6 @@ respective page.
The field ``results`` contains a list of objects representing the first results. For most
objects, every page contains 50 results.
Conditional fetching
--------------------
If you pull object lists from pretix' APIs regularly, we ask you to implement conditional fetching
to avoid unnecessary data traffic. This is not supported on all resources and we currently implement
two different mechanisms for different resources, which is necessary because we can only obtain best
efficiency for resources that do not support deletion operations.
Object-level conditional fetching
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :ref:`rest-orders` resource list contains an HTTP header called ``X-Page-Generated`` containing the
current time on the server in ISO 8601 format. On your next request, you can pass this header
(as is, without any modifications necessary) as the ``modified_since`` query parameter and you will receive
a list containing only objects that have changed in the time since your last request.
List-level conditional fetching
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If modification checks are not possible with this granularity, you can instead check for the full list.
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
last modification to any item of that resource. You can then pass this date back in your next request in the
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
``304 Not Modified`` return code.
This is currently implemented on the following resources:
* :ref:`rest-categories`
* :ref:`rest-items`
* :ref:`rest-questions`
* :ref:`rest-quotas`
* :ref:`rest-subevents`
* :ref:`rest-taxrules`
Errors
------

View File

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

View File

@@ -1,171 +0,0 @@
.. _`rest-oauth`:
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
that allows the user to easily set up a connection between the two systems.
If you haven't worked with OAuth before, have a look at the `OAuth2 Simplified`_ tutorial.
Registering an application
--------------------------
To use OAuth, you need to register your application with the pretix instance you want to connect to.
In order to do this, log in to your pretix account and go to your user settings. Click on "Authorized applications"
first and then on "Manage your own apps". From there, you can "Create a new application".
You should fill in a descriptive name of your application that allows users to recognize who you are. You also need to
give a list of fully-qualified URLs that users will be redirected to after a successful authorization. After you pressed
"Save", you will be presented with a client ID and a client secret. Please note them down and treat the client secret
like a password; it should not become available to your users.
Obtaining an authorization grant
--------------------------------
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
``response_type`` parameter with a value of ``code``. Example::
https://pretix.eu/api/v1/oauth/authorize?client_id=lsLi0hNL0vk53mEdYjNJxHUn1PcO1R6wVg81dLNT&response_type=code&scope=read+write&redirect_uri=https://pretalx.com
To prevent CSRF attacks, you can also optionally pass a ``state`` parameter with a random string. Later, when
redirecting back to your application, we will pass the same ``state`` parameter back to you, so you can compare if they
match.
After the user granted or denied access, they will be redirected back either to the ``redirect_url`` you passed in the
query or to the first redirect URL configured in your application settings.
On successful registration, we will append the query parameter ``code`` to the URL containing an authorization code.
For example, we might redirect the user to this URL::
https://pretalx.com/?code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&state=e3KCh9mfx07qxU4bRpXk
You will need this ``code`` parameter to perform the next step.
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
given and would therefore be unable to review their organizer restriction settings. You can append the
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
authorization.
Getting an access token
-----------------------
Using the ``code`` value you obtained above and your client ID, you can now request an access token that actually gives
access to the API. The ``token`` endpoint expects you to authenticate using `HTTP Basic authentication`_ using your client
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
parameter that you used for the authorization.
.. http:get:: /api/v1/oauth/token
Request a new access token
**Example request**:
.. sourcecode:: http
POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
grant_type=authorization_code&code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&redirect_uri=https://pretalx.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"access_token": "i3ytqTSRWsKp16fqjekHXa4tdM4qNC",
"expires_in": 86400,
"token_type": "Bearer",
"scope": "read write",
"refresh_token": "XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp"
}
:statuscode 200: no error
:statuscode 401: Authentication failure
As you can see, you receive two types of tokens: One "access token", and one "refresh token". The access token is valid
for a day and can be used to actually access the API. The refresh token does not have an expiration date and can be used
to obtain a new access_token after a day, so you should make sure to store the access token safely if you need long-term
access.
Using the API with an access token
----------------------------------
You can supply a valid access token as a ``Bearer``-type token in the ``Authorization`` header to get API access.
.. sourcecode:: http
:emphasize-lines: 3
GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
Refreshing an access token
--------------------------
You can obtain a new access token using your refresh token any time. This can be done using the same ``token`` endpoint
used to obtain the first access token above, but with a different set of parameters:
.. sourcecode:: http
POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
grant_type=refresh_token&refresh_token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
The previous access token will instantly become invalid.
Revoking a token
----------------
If you don't need a token any more or if you believe it may have been compromised, you can use the ``revoke_token``
endpoint to revoke it.
.. http:get:: /api/v1/oauth/revoke_token
Revoke an access or refresh token. If you revoke an access token, you can still create a new one using the refresh token. If you
revoke a refresh token, the connected access token will also be revoked.
**Example request**:
.. sourcecode:: http
POST /api/v1/oauth/revoke_token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
:statuscode 200: no error
:statuscode 401: Authentication failure
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.
.. _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

View File

@@ -1,258 +0,0 @@
.. _rest-carts:
Cart positions
==============
The API provides limited access to the cart position data model. This API currently only allows creating and deleting
cart positions to reserve quota.
Cart position resource
----------------------
The cart position resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the cart position
cart_id string Identifier of the cart this belongs to. Needs to end
in "@api" for API-created positions.
datetime datetime Time of creation
expires datetime The cart position will expire at this time and no longer block quota
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_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``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
===================================== ========================== =======================================================
.. versionchanged:: 1.17
This resource has been added.
Cart position endpoints
-----------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
Returns a list of API-created cart positions.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ 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
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_email": null,
"voucher": null,
"addon_to": null,
"subevent": null,
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
"includes_tax": true,
"answers": []
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
Returns information on one cart position, identified by its internal ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/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": 1,
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name": null,
"attendee_email": null,
"voucher": null,
"addon_to": null,
"subevent": null,
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
"includes_tax": true,
"answers": []
}
: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 position 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 cart position does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
Creates a new cart position.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. warning::
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend.
There is a lot that it does not or can not do, and you will need to be careful using it.
It allows to bypass many of the restrictions imposed when creating a cart through the
regular shop.
Specifically, this endpoint currently
* does not validate if products are only to be sold in a specific time frame
* does not validate if the event's ticket sales are already over or haven't started
* does not support add-on products at the moment
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not prevent you from buying items that can only be bought with a voucher
* does not support file upload questions
You can supply the following fields of the resource:
* ``cart_id`` (optional, needs to end in ``@api``)
* ``item``
* ``variation`` (optional)
* ``price``
* ``attendee_name`` (optional)
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
* ``includes_tax`` (optional)
* ``answers``
* ``question``
* ``answer``
* ``options``
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": null,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
(Full cart position resource, see above.)
:param organizer: The ``slug`` field of the organizer of the event to create a position for
:param event: The ``slug`` field of the event to create a position for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
Deletes a cart position, identified by its internal ID.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/cartpositions/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
Content-Type: application/json
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the position to delete
: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 cart position does not exist.

View File

@@ -1,5 +1,3 @@
.. _`rest-categories`:
Item categories
===============
@@ -16,7 +14,6 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the category
name multi-lingual string The category's visible name
internal_name string An optional name that is only used in the backend
description multi-lingual string A public description (might include markdown, can
be ``null``)
position integer An integer, used for sorting the categories
@@ -29,10 +26,6 @@ is_addon boolean If ``True``, it
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 1.16
The field ``internal_name`` has been added.
Endpoints
---------
@@ -65,7 +58,6 @@ Endpoints
{
"id": 1,
"name": {"en": "Tickets"},
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
@@ -107,7 +99,6 @@ Endpoints
{
"id": 1,
"name": {"en": "Tickets"},
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
@@ -135,7 +126,6 @@ Endpoints
{
"name": {"en": "Tickets"},
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
@@ -152,7 +142,6 @@ Endpoints
{
"id": 1,
"name": {"en": "Tickets"},
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
@@ -198,7 +187,6 @@ Endpoints
{
"id": 1,
"name": {"en": "Tickets"},
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": true

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
@@ -379,7 +375,6 @@ Order position endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 1,
@@ -426,8 +421,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
@@ -474,7 +467,6 @@ Order position endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 1,

View File

@@ -20,4 +20,3 @@ Resources and endpoints
vouchers
checkinlists
waitinglist
carts

View File

@@ -1,5 +1,3 @@
.. _rest-items:
Items
=====
@@ -16,7 +14,6 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the item
name multi-lingual string The item's visible name
internal_name string An optional name that is only used in the backend
default_price money (string) The item price that is applied if the price is not
overwritten by variations or other options.
category integer The ID of the category this item belongs to
@@ -57,17 +54,11 @@ max_per_order integer This product ca
checkin_attention boolean If ``True``, the check-in app should show a warning
that this ticket requires special attention if such
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,
use separate endpoint to modify this later.
├ id integer Internal ID of the variation
├ value multi-lingual string The "name" of the variation
├ default_price money (string) The price set directly for this variation or ``null``
├ price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
@@ -97,14 +88,6 @@ addons list of objects Definition of a
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
The attribute ``price_included`` has been added to ``addons``.
.. versionchanged:: 1.16
The ``internal_name`` and ``original_price`` fields have been added.
.. versionchanged:: 2.0
The field ``require_approval`` has been added.
Notes
-----
Please note that an item either always has variations or never has. Once created with variations the item can never
@@ -146,9 +129,7 @@ Endpoints
{
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"default_price": "23.00",
"original_price": null,
"category": null,
"active": true,
"description": null,
@@ -167,7 +148,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -231,9 +211,7 @@ Endpoints
{
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"default_price": "23.00",
"original_price": null,
"category": null,
"active": true,
"description": null,
@@ -252,7 +230,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -297,9 +274,7 @@ Endpoints
{
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"default_price": "23.00",
"original_price": null,
"category": null,
"active": true,
"description": null,
@@ -317,7 +292,6 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -350,9 +324,7 @@ Endpoints
{
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"default_price": "23.00",
"original_price": null,
"category": null,
"active": true,
"description": null,
@@ -371,7 +343,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},
@@ -435,9 +406,7 @@ Endpoints
{
"id": 1,
"name": {"en": "Ticket"},
"internal_name": "",
"default_price": "25.00",
"original_price": null,
"category": null,
"active": true,
"description": null,
@@ -456,7 +425,6 @@ Endpoints
"max_per_order": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"variations": [
{
"value": {"en": "Student"},

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
.. spelling:: checkin
.. _rest-questions:
Questions
=========

View File

@@ -1,5 +1,3 @@
.. _rest-quotas:
Quotas
======

View File

@@ -1,5 +1,3 @@
.. _rest-subevents:
Event series dates / Sub-events
===============================

View File

@@ -1,5 +1,3 @@
.. _rest-taxrules:
Tax rules
=========

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
.. 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_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:: order_can_retry
.. automethod:: payment_prepare
.. automethod:: order_prepare
.. automethod:: payment_control_render
.. automethod:: order_paid_render
.. automethod:: payment_refund_supported
.. automethod:: order_control_render
.. automethod:: payment_partial_refund_supported
.. automethod:: order_control_refund_render
.. automethod:: execute_refund
.. 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
@@ -118,15 +122,13 @@ for example, to check for any errors in any staged files when committing::
export GIT_WORK_TREE=../
export GIT_DIR=../.git
source ../env/bin/activate # Adjust to however you activate your virtual environment
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py")
for file in $(git diff --cached --name-only | grep -E '\.py$')
do
echo $file
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
done
This keeps you from accidentally creating commits violating the style guide.
Working with mails

View File

@@ -1,110 +0,0 @@
Badges
======
The badges plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
Resource description
--------------------
The badge layout resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal layout ID
name string Internal layout description
default boolean ``true`` if this is the default layout
layout object Layout specification for libpretixprint
background URL Background PDF file
item_assignments list of objects Products this layout is assigned to
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/
Returns a list of all badge layouts
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/badgelayouts/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": {…},
"background": {},
"item_assignments": []
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/(id)/
Returns information on layout.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/layoutsbadge/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: text/javascript
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": {…},
"background": {},
"item_assignments": []
}
: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 layout to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -12,5 +12,3 @@ If you want to **create** a plugin, please go to the
list
pretixdroid
banktransfer
ticketoutputpdf
badges

View File

@@ -81,7 +81,6 @@ uses to communicate with the pretix server.
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"addons_text": "Parking spot",
"paid": true
}
}
@@ -107,7 +106,6 @@ uses to communicate with the pretix server.
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"addons_text": "Parking spot",
"paid": true
},
"questions": [
@@ -154,7 +152,6 @@ uses to communicate with the pretix server.
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"addons_text": "Parking spot",
"paid": true
}
}
@@ -215,7 +212,6 @@ uses to communicate with the pretix server.
"redeemed": false,
"attention": false,
"checkin_allowed": true,
"addons_text": "Parking spot",
"paid": true
},
...

View File

@@ -1,111 +0,0 @@
PDF ticket output
=================
The PDF ticket output plugin provides a HTTP API that exposes the various layouts used
to generate PDF tickets.
Resource description
--------------------
The ticket layout resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal layout ID
name string Internal layout description
default boolean ``true`` if this is the default layout
layout object Layout specification for libpretixprint
background URL Background PDF file
item_assignments list of objects Products this layout is assigned to
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
Returns a list of all ticket layouts
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": {…},
"background": {},
"item_assignments": []
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
Returns information on layout.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/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: text/javascript
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": {…},
"background": {},
"item_assignments": []
}
: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 layout to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -38,12 +38,10 @@ gunicorn
hardcoded
hostname
idempotency
incrementing
inofficial
invalidations
iterable
Jimdo
libpretixprint
libsass
linters
memcached
@@ -79,7 +77,6 @@ prometheus
proxied
proxying
pseudonymize
pseudonymization
queryset
redemptions
redis

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

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.0.0"
__version__ = "1.15.2"

View File

@@ -1,9 +0,0 @@
from django.apps import AppConfig
class PretixApiConfig(AppConfig):
name = 'pretix.api'
label = 'pretixapi'
default_app_config = 'pretix.api.PretixApiConfig'

View File

@@ -1,6 +1,5 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
@@ -56,15 +55,6 @@ class EventPermission(BasePermission):
if required_permission and required_permission not in request.orgapermset:
return False
if isinstance(request.auth, OAuthAccessToken):
if not request.auth.allow_scopes(['write']) and request.method not in SAFE_METHODS:
return False
if not request.auth.allow_scopes(['read']) and request.method in SAFE_METHODS:
return False
if isinstance(request.auth, OAuthAccessToken) and hasattr(request, 'organizer'):
if not request.auth.organizers.filter(pk=request.organizer.pk).exists():
return False
return True

View File

@@ -1,128 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:19
from __future__ import unicode_literals
import django.db.models.deletion
import oauth2_provider.generators
import oauth2_provider.validators
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='OAuthAccessToken',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('token', models.CharField(max_length=255, unique=True)),
('expires', models.DateTimeField()),
('scope', models.TextField(blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthApplication',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('client_type',
models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
('authorization_grant_type', models.CharField(
choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'),
('password', 'Resource owner password-based'),
('client-credentials', 'Client credentials')], max_length=32)),
('skip_authorization', models.BooleanField(default=False)),
('created', models.DateTimeField(auto_now_add=True)),
('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],
verbose_name='Redirection URIs')),
('client_id',
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100,
unique=True, verbose_name='Client ID')),
('client_secret',
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_secret,
max_length=255, verbose_name='Client secret')),
('active', models.BooleanField(default=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthapplication', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthGrant',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('code', models.CharField(max_length=255, unique=True)),
('expires', models.DateTimeField()),
('redirect_uri', models.CharField(max_length=255)),
('scope', models.TextField(blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthgrant',
to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthRefreshToken',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('token', models.CharField(max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('revoked', models.DateTimeField(null=True)),
('access_token',
models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthrefreshtoken', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='oauthaccesstoken',
name='application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
),
migrations.AddField(
model_name='oauthaccesstoken',
name='source_refresh_token',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='refreshed_access_token',
to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
),
migrations.AddField(
model_name='oauthaccesstoken',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthaccesstoken', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='oauthrefreshtoken',
unique_together=set([('token', 'revoked')]),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0001_initial'),
('pretixapi', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='oauthaccesstoken',
name='organizers',
field=models.ManyToManyField(to='pretixbase.Organizer'),
),
migrations.AddField(
model_name='oauthgrant',
name='organizers',
field=models.ManyToManyField(to='pretixbase.Organizer'),
),
]

View File

@@ -1,70 +0,0 @@
from datetime import timedelta
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from oauth2_provider.generators import (
generate_client_id, generate_client_secret,
)
from oauth2_provider.models import (
AbstractAccessToken, AbstractApplication, AbstractGrant,
AbstractRefreshToken,
)
from oauth2_provider.validators import URIValidator
class OAuthApplication(AbstractApplication):
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
redirect_uris = models.TextField(
blank=False, validators=[URIValidator],
verbose_name=_("Redirection URIs"),
help_text=_("Allowed URIs list, space separated")
)
client_id = models.CharField(
verbose_name=_("Client ID"),
max_length=100, unique=True, default=generate_client_id, db_index=True
)
client_secret = models.CharField(
verbose_name=_("Client secret"),
max_length=255, blank=False, default=generate_client_secret, db_index=True
)
active = models.BooleanField(default=True)
def get_absolute_url(self):
return reverse("control:user.settings.oauth.app", kwargs={'pk': self.id})
def is_usable(self, request):
return self.active and super().is_usable(request)
class OAuthGrant(AbstractGrant):
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE
)
organizers = models.ManyToManyField('pretixbase.Organizer')
class OAuthAccessToken(AbstractAccessToken):
source_refresh_token = models.OneToOneField(
# unique=True implied by the OneToOneField
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
related_name="refreshed_access_token"
)
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
)
organizers = models.ManyToManyField('pretixbase.Organizer')
def revoke(self):
self.expires = now() - timedelta(hours=1)
self.save(update_fields=['expires'])
class OAuthRefreshToken(AbstractRefreshToken):
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE)
access_token = models.OneToOneField(
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
related_name="refresh_token"
)

View File

@@ -1,45 +0,0 @@
from datetime import timedelta
from django.utils import timezone
from oauth2_provider.exceptions import FatalClientError
from oauth2_provider.oauth2_validators import Grant, OAuth2Validator
from oauth2_provider.settings import oauth2_settings
class Validator(OAuth2Validator):
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
if not getattr(request, 'organizers', None):
raise FatalClientError('No organizers selected.')
expires = timezone.now() + timedelta(
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
g = Grant(application=request.client, user=request.user, code=code["code"],
expires=expires, redirect_uri=request.redirect_uri,
scope=" ".join(request.scopes))
g.save()
g.organizers.add(*request.organizers.all())
def validate_code(self, client_id, code, client, request, *args, **kwargs):
try:
grant = Grant.objects.get(code=code, application=client)
if not grant.is_expired():
request.scopes = grant.scope.split(" ")
request.user = grant.user
request.organizers = grant.organizers.all()
return True
return False
except Grant.DoesNotExist:
return False
def _create_access_token(self, expires, request, token, source_refresh_token=None):
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
raise FatalClientError('No organizers selected.')
if hasattr(request, 'organizers'):
orgs = list(request.organizers.all())
else:
orgs = list(source_refresh_token.access_token.organizers.all())
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
access_token.organizers.add(*orgs)
return access_token

View File

@@ -1,121 +0,0 @@
from datetime import timedelta
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer,
)
from pretix.base.models import Quota
from pretix.base.models.orders import CartPosition
class CartPositionSerializer(I18nAwareModelSerializer):
answers = AnswerSerializer(many=True)
class Meta:
model = CartPosition
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)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers',)
def create(self, validated_data):
answers_data = validated_data.pop('answers')
if not validated_data.get('cart_id'):
cid = "{}@api".format(get_random_string(48))
while CartPosition.objects.filter(cart_id=cid).exists():
cid = "{}@api".format(get_random_string(48))
validated_data['cart_id'] = cid
if not validated_data.get('expires'):
validated_data['expires'] = now() + timedelta(
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
)
with self.context['event'].lock():
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
for quota in new_quotas:
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
options = answ_data.pop('options')
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
return cp
def validate_cart_id(self, cid):
if cid and not cid.endswith('@api'):
raise ValidationError('Cart ID should end in @api or be empty.')
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
'The specified item does not belong to this event.'
)
if not item.active:
raise ValidationError(
'The specified item is not active.'
)
return item
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
elif subevent:
raise ValidationError(
'You cannot set a subevent for this event.'
)
return subevent
def validate(self, data):
if data.get('item'):
if data.get('item').has_variations:
if not data.get('variation'):
raise ValidationError('You should specify a variation for this item.')
else:
if data.get('variation').item != data.get('item'):
raise ValidationError(
'The specified variation does not belong to the specified item.'
)
elif data.get('variation'):
raise ValidationError(
'You cannot specify a variation for this item.'
)
return data

View File

@@ -74,12 +74,12 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
fields = ('id', 'category', 'name', 'active', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'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')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
@@ -129,7 +129,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
class Meta:
model = ItemCategory
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
fields = ('id', 'name', 'description', 'position', 'is_addon')
class QuestionOptionSerializer(I18nAwareModelSerializer):

View File

@@ -1,31 +1,18 @@
import json
from collections import Counter
from decimal import Decimal
from django.utils.timezone import now
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
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
QuestionAnswer,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
)
from pretix.base.pdf import get_variables
from pretix.base.models.orders import OrderFee
from pretix.base.signals import register_ticket_outputs
class CompatibleCountryField(serializers.Field):
def to_internal_value(self, data):
return {self.field_name: Country(data)}
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
@@ -40,13 +27,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
model = InvoiceAddress
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):
super().__init__(*args, **kwargs)
for v in self.fields.values():
v.required = False
v.allow_blank = True
class AnswerQuestionIdentifierField(serializers.Field):
@@ -124,59 +104,17 @@ class PositionDownloadsField(serializers.Field):
return res
class PdfDataSerializer(serializers.Field):
def to_representation(self, instance: OrderPosition):
res = {}
ev = instance.subevent or instance.order.event
pdfvars = get_variables(instance.order.event)
for k, f in pdfvars.items():
res[k] = f['evaluate'](instance, instance.order, ev)
for k, v in ev.meta_data.items():
res['meta:' + k] = v
return res
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True)
answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*')
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*')
class Meta:
model = OrderPosition
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)
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
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())
'answers', 'tax_rule')
class OrderFeeSerializer(I18nAwareModelSerializer):
@@ -185,18 +123,16 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
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 PaymentFeeLegacyField(serializers.Field):
def __init__(self, *args, **kwargs):
self.attr = kwargs.pop('attribute')
super().__init__(*args, **kwargs)
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')
def to_representation(self, instance: Order):
return str(
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
Decimal('0.00'))
)
class OrderSerializer(I18nAwareModelSerializer):
@@ -204,367 +140,12 @@ class OrderSerializer(I18nAwareModelSerializer):
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')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields['positions'].child.fields.pop('pdf_data')
class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
raise ValidationError(
'File uploads are currently not supported via the API.'
)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
def validate_tax_rule(self, tr):
if tr and tr.event != self.context['event']:
raise ValidationError(
'The specified tax rate does not belong to this event.'
)
return tr
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret):
if secret and OrderPosition.objects.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError(
'You cannot assign a position secret that already exists.'
)
return secret
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
'The specified item does not belong to this event.'
)
if not item.active:
raise ValidationError(
'The specified item is not active.'
)
return item
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
elif subevent:
raise ValidationError(
'You cannot set a subevent for this event.'
)
return subevent
def validate(self, data):
if data.get('item'):
if data.get('item').has_variations:
if not data.get('variation'):
raise ValidationError({'variation': ['You should specify a variation for this item.']})
else:
if data.get('variation').item != data.get('item'):
raise ValidationError(
{'variation': ['The specified variation does not belong to the specified item.']}
)
elif data.get('variation'):
raise ValidationError(
{'variation': ['You cannot specify a variation for this item.']}
)
return data
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=False)
fees = OrderFeeCreateSerializer(many=True, required=False)
status = serializers.ChoiceField(choices=(
('n', Order.STATUS_PENDING),
('p', Order.STATUS_PAID),
), default='n', required=False)
code = serializers.CharField(
required=False,
max_length=16,
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
payment_provider = serializers.CharField(required=True)
payment_info = CompatibleJSONField(required=False)
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
def validate_payment_provider(self, pp):
if pp not in self.context['event'].get_payment_providers():
raise ValidationError('The given payment provider is not known.')
return pp
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(
'This order code is already in use.'
)
if any(c not in 'ABCDEFGHJKLMNPQRSTUVWXYZ1234567890' for c in code):
raise ValidationError(
'This order code contains invalid characters.'
)
return code
def validate_positions(self, data):
if not data:
raise ValidationError(
'An order cannot be empty.'
)
errs = [{} for p in data]
if any([p.get('positionid') for p in data]):
if not all([p.get('positionid') for p in data]):
for i, p in enumerate(data):
if not p.get('positionid'):
errs[i]['positionid'] = [
'If you set position IDs manually, you need to do so for all positions.'
]
raise ValidationError(errs)
last_non_add_on = None
last_posid = 0
for i, p in enumerate(data):
if p['positionid'] != last_posid + 1:
errs[i]['positionid'] = [
'Position IDs need to be consecutive.'
]
if p.get('addon_to') and p['addon_to'] != last_non_add_on:
errs[i]['addon_to'] = [
"If you set addon_to, you need to make sure that the referenced "
"position ID exists and is transmitted directly before its add-ons."
]
if not p.get('addon_to'):
last_non_add_on = p['positionid']
last_posid = p['positionid']
elif any([p.get('addon_to') for p in data]):
errs = [
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
for p in data
]
if any(errs):
raise ValidationError(errs)
return data
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:
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
else:
ia = None
with self.context['event'].lock() as now_dt:
quotadiff = Counter()
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
if consume_carts:
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
for quota in quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] += 1
if cp.expires > now_dt:
quotadiff.subtract(quotas)
delete_cps.append(cp)
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
quotadiff.update(new_quotas)
if any(errs):
raise ValidationError({'positions': errs})
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
order.meta_info = "{}"
order.save()
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
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'):
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 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)
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
if addon_to:
pos.addon_to = pos_map[addon_to]
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
for cp in delete_cps:
cp.delete()
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
return order
'checkin_attention')
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
@@ -584,27 +165,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

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

@@ -4,11 +4,7 @@ from django.apps import apps
from django.conf.urls import include, url
from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
)
from .views import checkin, event, item, order, organizer, voucher, waitinglist
router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
@@ -30,7 +26,6 @@ event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
@@ -42,10 +37,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'):
@@ -61,8 +52,4 @@ 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"),
]

View File

@@ -1,8 +1,3 @@
from calendar import timegm
from django.db.models import Max
from django.http import HttpResponse
from django.utils.http import http_date, parse_http_date_safe
from rest_framework.filters import OrderingFilter
@@ -26,33 +21,3 @@ class RichOrderingFilter(OrderingFilter):
return queryset.order_by(*ordering)
return queryset
class ConditionalListView:
def list(self, request, **kwargs):
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
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,
).aggregate(
m=Max('datetime')
)['m']
if lmd:
lmd_ts = timegm(lmd.utctimetuple())
if if_unmodified_since and lmd and lmd_ts > if_unmodified_since:
return HttpResponse(status=412)
if if_modified_since and lmd and lmd_ts <= if_modified_since:
return HttpResponse(status=304)
resp = super().list(request, **kwargs)
if lmd:
resp['Last-Modified'] = http_date(lmd_ts)
return resp

View File

@@ -1,46 +0,0 @@
from django.db import transaction
from rest_framework import status, viewsets
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from pretix.api.serializers.cart import (
CartPositionCreateSerializer, CartPositionSerializer,
)
from pretix.base.models import CartPosition
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CartPositionSerializer
queryset = CartPosition.objects.none()
filter_backends = (OrderingFilter,)
ordering = ('datetime',)
ordering_fields = ('datetime', 'cart_id')
lookup_field = 'id'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_queryset(self):
return CartPosition.objects.filter(
event=self.request.event,
cart_id__endswith="@api"
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def create(self, request, *args, **kwargs):
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
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()

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
@@ -17,6 +16,7 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
@@ -33,7 +33,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'
@@ -49,7 +49,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.checkinlist.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -63,7 +63,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.checkinlist.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -71,7 +71,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.event.checkinlist.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@@ -176,16 +176,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(

View File

@@ -9,9 +9,9 @@ from pretix.api.serializers.event import (
CloneEventSerializer, EventSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import Event, ItemCategory, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -38,7 +38,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
log_action,
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -51,7 +51,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.plugins.' + action,
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'plugin': module}
)
@@ -60,7 +60,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -69,7 +69,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -114,7 +114,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -125,11 +125,11 @@ class SubEventFilter(FilterSet):
fields = ['active']
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = SubEventFilter
filter_class = SubEventFilter
def get_queryset(self):
return self.request.event.subevents.prefetch_related(
@@ -137,7 +137,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
)
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class TaxRuleViewSet(viewsets.ModelViewSet):
serializer_class = TaxRuleSerializer
queryset = TaxRule.objects.none()
write_permission = 'can_change_event_settings'
@@ -150,7 +150,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.taxrule.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -159,7 +159,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.taxrule.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -170,6 +170,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.taxrule.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -13,11 +13,11 @@ from pretix.api.serializers.item import (
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -35,13 +35,13 @@ class ItemFilter(FilterSet):
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filterset_class = ItemFilter
filter_class = ItemFilter
permission = 'can_change_items'
write_permission = 'can_change_items'
@@ -53,7 +53,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.item.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -68,7 +68,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.item.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -81,7 +81,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.item.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@@ -113,7 +113,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
item.log_action(
'pretix.event.item.variation.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
@@ -123,7 +123,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
serializer.instance.item.log_action(
'pretix.event.item.variation.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
@@ -140,7 +140,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
instance.item.log_action(
'pretix.event.item.variation.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={
'value': instance.value,
'id': self.kwargs['pk']
@@ -174,7 +174,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
item.log_action(
'pretix.event.item.addons.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -183,7 +183,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer.instance.base_item.log_action(
'pretix.event.item.addons.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -192,7 +192,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
instance.base_item.log_action(
'pretix.event.item.addons.removed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'category': instance.addon_category.pk}
)
@@ -203,11 +203,11 @@ class ItemCategoryFilter(FilterSet):
fields = ['is_addon']
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
class ItemCategoryViewSet(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 = 'can_change_items'
@@ -221,7 +221,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.category.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -235,7 +235,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.category.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -246,7 +246,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.category.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@@ -257,11 +257,11 @@ class QuestionFilter(FilterSet):
fields = ['ask_during_checkin', 'required', 'identifier']
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
class QuestionViewSet(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 = 'can_change_items'
@@ -274,7 +274,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.question.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -288,7 +288,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.question.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -296,7 +296,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.question.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@@ -326,7 +326,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
q.log_action(
'pretix.event.question.option.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -335,7 +335,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer.instance.question.log_action(
'pretix.event.question.option.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -343,7 +343,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
instance.question.log_action(
'pretix.event.question.option.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'id': instance.pk}
)
super().perform_destroy(instance)
@@ -355,11 +355,11 @@ class QuotaFilter(FilterSet):
fields = ['subevent']
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
class QuotaViewSet(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 = 'can_change_items'
@@ -373,14 +373,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.quota.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if serializer.instance.subevent:
serializer.instance.subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -396,7 +396,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.quota.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if current_subevent == request_subevent:
@@ -404,7 +404,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
current_subevent.log_action(
'pretix.subevent.quota.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
else:
@@ -412,14 +412,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
request_subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
if current_subevent is not None:
current_subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
serializer.instance.rebuild_cache()
@@ -427,13 +427,13 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.quota.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
if instance.subevent:
instance.subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -1,92 +0,0 @@
import logging
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.forms import AllowForm
from oauth2_provider.views import (
AuthorizationView as BaseAuthorizationView,
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
)
from pretix.api.models import OAuthApplication
from pretix.base.models import Organizer
logger = logging.getLogger(__name__)
class OAuthAllowForm(AllowForm):
organizers = forms.ModelMultipleChoiceField(
queryset=Organizer.objects.none(),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['organizers'].queryset = Organizer.objects.filter(
pk__in=user.teams.values_list('organizer', flat=True))
class AuthorizationView(BaseAuthorizationView):
template_name = "pretixcontrol/auth/oauth_authorization.html"
form_class = OAuthAllowForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['settings'] = settings
return ctx
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
credentials["organizers"] = organizers
return super().create_authorization_response(request, scopes, credentials, allow)
def form_valid(self, form):
client_id = form.cleaned_data["client_id"]
application = OAuthApplication.objects.get(client_id=client_id)
credentials = {
"client_id": form.cleaned_data.get("client_id"),
"redirect_uri": form.cleaned_data.get("redirect_uri"),
"response_type": form.cleaned_data.get("response_type", None),
"state": form.cleaned_data.get("state", None),
}
scopes = form.cleaned_data.get("scope")
allow = form.cleaned_data.get("allow")
try:
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=scopes, credentials=credentials, allow=allow,
organizers=form.cleaned_data.get("organizers")
)
except OAuthToolkitError as error:
return self.error_response(error, application)
self.success_url = uri
logger.debug("Success url for the request: {0}".format(self.success_url))
msgs = [
_('The application "{application_name}" has been authorized to access your account.').format(
application_name=application.name
)
]
self.request.user.send_security_notice(msgs)
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
'application_id': application.pk,
'application_name': application.name,
})
return self.redirect(self.success_url, application)
class TokenView(BaseTokenView):
pass
class RevokeTokenView(BaseRevokeTokenView):
pass

View File

@@ -2,79 +2,63 @@ import datetime
import django_filters
import pytz
from django.db import transaction
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.utils.timezone import make_aware
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,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
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, OrderPositionSerializer, OrderSerializer,
)
from pretix.base.models import (
Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.models import Invoice, Order, OrderPosition, Quota
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
generate_cancellation, generate_invoice, invoice_pdf, 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,
)
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.signals import order_placed, register_ticket_outputs
from pretix.base.signals import 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')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'require_approval']
fields = ['code', 'status', 'email', 'locale']
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status')
filterset_class = OrderFilter
filter_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'positions__answers__question', 'fees', 'payments', 'refunds', 'refunds__payment'
'positions__answers__question', 'fees'
).select_related(
'invoice_address'
)
@@ -87,20 +71,6 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return prov
raise NotFound('Unknown output provider.')
def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
resp = self.get_paginated_response(serializer.data)
resp['X-Page-Generated'] = date
return resp
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
@@ -126,33 +96,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,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
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
@@ -176,48 +127,11 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
send_mail=send_mail
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def 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, (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, (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()
@@ -229,11 +143,12 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
)
order.status = Order.STATUS_PENDING
order.payment_manual = True
order.save()
order.log_action(
'pretix.event.order.unpaid',
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
return self.retrieve(request, [], **kwargs)
@@ -248,29 +163,14 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
)
mark_order_expired(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
def mark_refunded(self, request, **kwargs):
order = self.get_object()
if order.status != Order.STATUS_PAID:
return Response(
{'detail': 'The order is not paid.'},
status=status.HTTP_400_BAD_REQUEST
)
mark_order_refunded(
order,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
return self.retrieve(request, [], **kwargs)
# TODO: Find a way to implement mark_refunded
@detail_route(methods=['POST'])
def extend(self, request, **kwargs):
new_date = request.data.get('expires', None)
@@ -304,7 +204,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
new_date=new_date,
force=force,
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
return self.retrieve(request, [], **kwargs)
except OrderError as e:
@@ -313,37 +213,9 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
def create(self, request, *args, **kwargs):
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
if gen_invoice:
generate_invoice(order, trigger_pdf=True)
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 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')
@@ -371,22 +243,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'
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
@@ -427,232 +295,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()
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()
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(
@@ -679,7 +326,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'
@@ -723,7 +370,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
'invoice': inv.pk
},
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
return Response(status=204)
@@ -746,6 +393,6 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
'invoice': inv.pk
},
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
return Response(status=204)

View File

@@ -1,6 +1,5 @@
from rest_framework import viewsets
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import OrganizerSerializer
from pretix.base.models import Organizer
@@ -12,15 +11,9 @@ 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):
return Organizer.objects.filter(
pk__in=self.request.user.teams.values_list('organizer', flat=True)
).filter(
pk__in=self.request.auth.organizers.values_list('pk', flat=True)
)
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
else:

View File

@@ -9,6 +9,7 @@ from rest_framework.filters import OrderingFilter
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
from pretix.base.models.organizer import TeamAPIToken
class VoucherFilter(FilterSet):
@@ -34,7 +35,7 @@ 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'
@@ -50,7 +51,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -68,7 +69,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.voucher.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
@@ -79,6 +80,6 @@ class VoucherViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.voucher.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -7,7 +7,7 @@ from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import WaitingListEntry
from pretix.base.models import TeamAPIToken, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
@@ -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'
@@ -45,7 +45,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.orders.waitinglist.added',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
def perform_update(self, serializer):
@@ -55,7 +55,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.orders.waitinglist.changed',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
def perform_destroy(self, instance):
@@ -65,7 +65,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.event.orders.waitinglist.deleted',
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
@@ -74,7 +74,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
try:
self.get_object().send_voucher(
user=self.request.user,
auth=self.request.auth,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
except WaitingListException as e:
raise ValidationError(str(e))

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 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')
@@ -35,103 +24,3 @@ class CustomSMTPBackend(EmailBackend):
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 NotImplemented
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('pk')) & 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')
@@ -48,19 +38,12 @@ class InvoiceExporter(BaseExporter):
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
try:
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()
except FileNotFoundError:
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
@@ -94,10 +77,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

@@ -24,15 +24,13 @@ class JSONExporter(BaseExporter):
'categories': [
{
'id': category.id,
'name': str(category.name),
'internal_name': category.internal_name
'name': str(category.name)
} for category in self.event.categories.all()
],
'items': [
{
'id': item.id,
'name': str(item.name),
'internal_name': str(item.internal_name),
'category': item.category_id,
'price': item.default_price,
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),

View File

@@ -5,13 +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.models.orders import OrderFee
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -55,19 +55,7 @@ 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)
@@ -75,7 +63,7 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
_('Payment date'), _('Payment type'), _('Fees'), _('Order locale')
]
for tr in tax_rates:
@@ -89,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'))
@@ -121,8 +114,7 @@ class OrderListExporter(BaseExporter):
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:
@@ -130,14 +122,14 @@ class OrderListExporter(BaseExporter):
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']),
@@ -152,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)')
@@ -259,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

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

@@ -199,16 +199,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if event.settings.invoice_name_required:
self.fields['name'].required = True
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']
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'
@@ -252,12 +242,3 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'resolve this manually.'))
else:
self.instance.vat_id_validated = False
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
if f != 'name':
del self.fields[f]

View File

@@ -110,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:
@@ -133,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

@@ -1,4 +1,3 @@
import logging
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -9,7 +8,6 @@ from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
@@ -28,8 +26,6 @@ from pretix.base.models import Event, Invoice
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
logger = logging.getLogger(__name__)
class BaseInvoiceRenderer:
"""
@@ -182,19 +178,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
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
)
return width, height
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
@@ -209,18 +192,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.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))
@@ -237,14 +208,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)
@@ -299,42 +276,25 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
ir = ThumbnailingImageReader(logo_file)
try:
ir.resize(25 * mm, 25 * mm, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
def shorten(txt):
txt = str(txt)
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
return txt
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
str(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display())
)
else:
p_str = (
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
)
else:
p_str = shorten(self.invoice.event.name)
p_str = str(self.invoice.event.name)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)

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
@@ -172,12 +172,6 @@ class SecurityMiddleware(MiddlewareMixin):
return resp
resp['X-XSS-Protection'] = '1'
# We just need to have a P3P, not matter whats in there
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],

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,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-05-09 09:17
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0089_auto_20180315_1322'),
]
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'),
),
]

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,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-05-13 16:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0090_auto_20180509_0917'),
]
operations = [
migrations.AddField(
model_name='order',
name='last_modified',
field=models.DateTimeField(auto_now=True, db_index=True),
),
]

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-05-11 12:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0091_auto_20180513_1641'),
]
operations = [
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'),
),
]

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-28 14:32
from __future__ import unicode_literals
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):
dependencies = [
('pretixbase', '0092_auto_20180511_1224'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='pseudonymization_id',
field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
),
migrations.RunPython(
set_pids,
migrations.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,
),
]

View File

@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:19
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0093_auto_20180528_1432'),
('pretixapi', '0001_initial')
]
operations = [
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:29
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0094_auto_20180604_1119'),
]
operations = [
migrations.AddField(
model_name='logentry',
name='oauth_application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixapi.OAuthApplication'),
),
]

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

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

@@ -25,13 +25,13 @@ class UserManager(BaseUserManager):
model documentation to see what's so special about our user model.
"""
def create_user(self, email: str, password: str = None, **kwargs):
def create_user(self, email: str, password: str=None, **kwargs):
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save()
return user
def create_superuser(self, email: str, password: str = None): # NOQA
def create_superuser(self, email: str, password: str=None): # NOQA
# Not used in the software but required by Django
if password is None:
raise Exception("You must provide a password")
@@ -93,7 +93,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
verbose_name=_('Timezone'))
require_2fa = models.BooleanField(
default=False,
verbose_name=_('Two-factor authentication is required to log in')
verbose_name=_('Two-factor authentification is required to log in')
)
notifications_send = 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

@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
class LoggingMixin:
def log_action(self, action, data=None, user=None, api_token=None, auth=None, save=True):
def log_action(self, action, data=None, user=None, api_token=None, save=True):
"""
Create a LogEntry object that is related to this object.
See the LogEntry documentation for details.
@@ -47,8 +47,6 @@ class LoggingMixin:
"""
from .log import LogEntry
from .event import Event
from pretix.api.models import OAuthAccessToken, OAuthApplication
from .organizer import TeamAPIToken
from ..notifications import get_all_notification_types
from ..services.notifications import notify
@@ -59,18 +57,7 @@ class LoggingMixin:
event = self.event
if user and not user.is_authenticated:
user = None
kwargs = {}
if isinstance(auth, OAuthAccessToken):
kwargs['oauth_application'] = auth.application
elif isinstance(auth, OAuthApplication):
kwargs['oauth_application'] = auth
elif isinstance(auth, TeamAPIToken):
kwargs['api_token'] = auth
elif isinstance(api_token, TeamAPIToken):
kwargs['api_token'] = api_token
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
if data:
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
if save:
@@ -96,4 +83,4 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token')
).select_related('user', 'event')

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(
@@ -168,11 +168,3 @@ class Checkin(models.Model):
return "<Checkin: pos {} on list '{}' at {}>".format(
self.position, self.list, self.datetime
)
def save(self, **kwargs):
self.position.order.touch()
super().save(**kwargs)
def delete(self, **kwargs):
self.position.order.touch()
super().delete(**kwargs)

View File

@@ -2,7 +2,6 @@ import string
import uuid
from collections import OrderedDict
from datetime import datetime, time
from operator import attrgetter
import pytz
from django.conf import settings
@@ -19,6 +18,7 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
@@ -264,7 +264,6 @@ class Event(EventMixin, LoggedModel):
verbose_name = _("Event")
verbose_name_plural = _("Events")
ordering = ("date_from", "name")
unique_together = (('organizer', 'slug'),)
def __str__(self):
return str(self.name)
@@ -326,8 +325,6 @@ class Event(EventMixin, LoggedModel):
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings.
"""
from pretix.base.email import CustomSMTPBackend
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
@@ -452,8 +449,10 @@ class Event(EventMixin, LoggedModel):
if int(s.value) in tax_map:
s.value = tax_map.get(int(s.value)).pk
s.save()
else:
s.delete()
except ValueError:
pass
s.delete()
else:
s.save()
@@ -480,31 +479,6 @@ class Event(EventMixin, LoggedModel):
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
def get_html_mail_renderer(self):
"""
Returns the currently selected HTML email renderer
"""
return self.get_html_mail_renderers()[
self.settings.mail_html_renderer
]
def get_html_mail_renderers(self) -> dict:
"""
Returns a dictionary of initialized HTML email renderers mapped by their identifiers.
"""
from ..signals import register_html_mail_renderers
responses = register_html_mail_renderers.send(self)
renderers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
if pp.is_available:
renderers[pp.identifier] = pp
return renderers
def get_invoice_renderers(self) -> dict:
"""
Returns a dictionary of initialized invoice renderers mapped by their identifiers.
@@ -561,28 +535,6 @@ class Event(EventMixin, LoggedModel):
)
).order_by('date_from', 'name')
@property
def subevent_list_subevents(self):
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
orderfields = {
'date_ascending': ('date_from', 'name'),
'date_descending': ('-date_from', 'name'),
'name_ascending': ('name', 'date_from'),
'name_descending': ('-name', 'date_from'),
}[ordering]
subevs = self.subevents.filter(
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(date_to__gte=now())
)
) # order_by doesn't make sense with I18nField
for f in reversed(orderfields):
if f.startswith('-'):
subevs = sorted(subevs, key=attrgetter(f[1:]), reverse=True)
else:
subevs = sorted(subevs, key=attrgetter(f))
return subevs
@property
def meta_data(self):
data = {p.name: p.default for p in self.organizer.meta_properties.all()}
@@ -593,7 +545,7 @@ class Event(EventMixin, LoggedModel):
def has_payment_provider(self):
result = False
for provider in self.get_payment_providers().values():
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting'):
if provider.is_enabled and provider.identifier != 'free':
result = True
break
return result

View File

@@ -64,14 +64,14 @@ class Invoice(models.Model):
:param file: The filename of the rendered invoice
:type file: File
"""
order = models.ForeignKey('Order', related_name='invoices', db_index=True, on_delete=models.CASCADE)
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', related_name='invoices', db_index=True, on_delete=models.CASCADE)
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
prefix = models.CharField(max_length=160, db_index=True)
invoice_no = models.CharField(max_length=19, db_index=True)
full_invoice_no = models.CharField(max_length=190, db_index=True)
is_cancellation = models.BooleanField(default=False)
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True, on_delete=models.CASCADE)
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
invoice_from = models.TextField()
invoice_to = models.TextField()
date = models.DateField(default=today)
@@ -175,7 +175,7 @@ class InvoiceLine(models.Model):
:param tax_name: The name of the applied tax rate
:type tax_name: str
"""
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
invoice = models.ForeignKey('Invoice', related_name='lines')
position = models.PositiveIntegerField(default=0)
description = models.TextField()
gross_value = models.DecimalField(max_digits=10, decimal_places=2)

View File

@@ -43,11 +43,6 @@ class ItemCategory(LoggedModel):
max_length=255,
verbose_name=_("Category name"),
)
internal_name = models.CharField(
verbose_name=_("Internal name"),
help_text=_("If you set this, this will be used instead of the public name in the backend."),
blank=True, null=True, max_length=255
)
description = I18nTextField(
blank=True, verbose_name=_("Category description")
)
@@ -68,10 +63,9 @@ class ItemCategory(LoggedModel):
ordering = ('position', 'id')
def __str__(self):
name = self.internal_name or self.name
if self.is_addon:
return _('{category} (Add-On products)').format(category=str(name))
return str(name)
return _('{category} (Add-On products)').format(category=str(self.name))
return str(self.name)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -191,10 +185,6 @@ class Item(LoggedModel):
:type min_per_order: int
:param checkin_attention: Requires special attention at check-in
:type checkin_attention: bool
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
:type require_approval: bool
"""
event = models.ForeignKey(
@@ -215,11 +205,6 @@ class Item(LoggedModel):
max_length=255,
verbose_name=_("Item name"),
)
internal_name = models.CharField(
verbose_name=_("Internal name"),
help_text=_("If you set this, this will be used instead of the public name in the backend."),
blank=True, null=True, max_length=255
)
active = models.BooleanField(
default=True,
verbose_name=_("Active"),
@@ -282,13 +267,6 @@ class Item(LoggedModel):
help_text=_('To buy this product, the user needs a voucher that applies to this product '
'either directly or via a quota.')
)
require_approval = models.BooleanField(
verbose_name=_('Buying this product requires approval'),
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.'),
)
hide_without_voucher = models.BooleanField(
verbose_name=_('This product will only be shown if a voucher matching the product is redeemed.'),
default=False,
@@ -322,15 +300,8 @@ class Item(LoggedModel):
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
max_digits=7, 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.')
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
# pretix/control/views/item.py if applicable.
class Meta:
verbose_name = _("Product")
@@ -338,7 +309,7 @@ class Item(LoggedModel):
ordering = ("category__position", "category", "position")
def __str__(self):
return str(self.internal_name or self.name)
return str(self.name)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
@@ -456,8 +427,7 @@ class ItemVariation(models.Model):
"""
item = models.ForeignKey(
Item,
related_name='variations',
on_delete=models.CASCADE
related_name='variations'
)
value = I18nCharField(
max_length=255,
@@ -572,14 +542,12 @@ class ItemAddOn(models.Model):
"""
base_item = models.ForeignKey(
Item,
related_name='addons',
on_delete=models.CASCADE
related_name='addons'
)
addon_category = models.ForeignKey(
ItemCategory,
related_name='addon_to',
verbose_name=_('Category'),
on_delete=models.CASCADE
verbose_name=_('Category')
)
min_count = models.PositiveIntegerField(
default=0,
@@ -691,8 +659,7 @@ class Question(LoggedModel):
event = models.ForeignKey(
Event,
related_name="questions",
on_delete=models.CASCADE
related_name="questions"
)
question = I18nTextField(
verbose_name=_("Question")
@@ -844,7 +811,7 @@ class Question(LoggedModel):
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
question = models.ForeignKey('Question', related_name='options')
identifier = models.CharField(max_length=190)
answer = I18nCharField(verbose_name=_('Answer'))
position = models.IntegerField(default=0)
@@ -1024,7 +991,7 @@ class Quota(LoggedModel):
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt
if self.size is None:
self.cached_availability_paid_orders = self.count_paid_orders()
self.cached_availability_paid_orders = self.count_pending_orders()
self.save(
update_fields=[
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',

View File

@@ -41,7 +41,6 @@ class LogEntry(models.Model):
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
@@ -52,7 +51,7 @@ class LogEntry(models.Model):
all = models.Manager()
class Meta:
ordering = ('-datetime', '-id')
ordering = ('-datetime',)
def display(self):
from ..signals import logentry_display
@@ -66,13 +65,10 @@ class LogEntry(models.Model):
def display_object(self):
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
try:
if self.content_type.model_class() is Event:
return ''
co = self.content_object
except:
if self.content_type.model_class() is Event:
return ''
co = self.content_object
a_map = None
a_text = None

View File

@@ -1,20 +1,16 @@
import copy
import json
import logging
import os
import string
from datetime import datetime, time, timedelta
from datetime import datetime, time
from decimal import Decimal
from typing import Any, Dict, List, Union
import dateutil
import pytz
from django.conf import settings
from django.db import models, transaction
from django.db.models import (
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
)
from django.db.models.functions import Coalesce
from django.db import models
from django.db.models import F, Sum
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
@@ -35,8 +31,6 @@ from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
logger = logging.getLogger(__name__)
def generate_secret():
return get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
@@ -82,14 +76,18 @@ class Order(LoggedModel):
:type datetime: datetime
:param expires: The date until this order has to be paid to guarantee the fulfillment
:type expires: datetime
:param payment_date: The date of the payment completion (null if not yet paid)
:type payment_date: datetime
:param payment_provider: The payment provider selected by the user
:type payment_provider: str
:param payment_info: Arbitrary information stored by the payment provider
:type payment_info: str
:param total: The total amount of the order, including the payment fee
:type total: decimal.Decimal
:param comment: An internal comment that will only be visible to staff, and never displayed to the user
:type comment: str
:param download_reminder_sent: A field to indicate whether a download reminder has been sent.
:type download_reminder_sent: boolean
:param require_approval: If set to ``True``, this order is pending approval by an organizer
:type require_approval: bool
:param meta_info: Additional meta information on the order, JSON-encoded.
:type meta_info: str
"""
@@ -121,8 +119,7 @@ class Order(LoggedModel):
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
related_name="orders",
on_delete=models.CASCADE
related_name="orders"
)
email = models.EmailField(
null=True, blank=True,
@@ -139,6 +136,23 @@ class Order(LoggedModel):
expires = models.DateTimeField(
verbose_name=_("Expiration date")
)
payment_date = models.DateTimeField(
verbose_name=_("Payment date"),
null=True, blank=True
)
payment_provider = models.CharField(
null=True, blank=True,
max_length=255,
verbose_name=_("Payment provider")
)
payment_info = models.TextField(
verbose_name=_("Payment information"),
null=True, blank=True
)
payment_manual = models.BooleanField(
verbose_name=_("Payment state was manually modified"),
default=False
)
total = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Total amount")
@@ -166,12 +180,6 @@ class Order(LoggedModel):
verbose_name=_("Meta information"),
null=True, blank=True
)
last_modified = models.DateTimeField(
auto_now=True, db_index=True
)
require_approval = models.BooleanField(
default=False
)
class Meta:
verbose_name = _("Order")
@@ -188,84 +196,6 @@ class Order(LoggedModel):
except TypeError:
return None
@property
def payment_refund_sum(self):
payment_sum = self.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
refund_sum = self.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
return payment_sum - refund_sum
@property
def pending_sum(self):
total = self.total
if self.status in (Order.STATUS_REFUNDED, Order.STATUS_CANCELED):
total = 0
payment_sum = self.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
refund_sum = self.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
return total - payment_sum + refund_sum
@classmethod
def annotate_overpayments(cls, qs):
payment_sum = OrderPayment.objects.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
order=OuterRef('pk')
).order_by().values('order').annotate(s=Sum('amount')).values('s')
refund_sum = OrderRefund.objects.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED),
order=OuterRef('pk')
).order_by().values('order').annotate(s=Sum('amount')).values('s')
external_refund = OrderRefund.objects.filter(
state=OrderRefund.REFUND_STATE_EXTERNAL,
order=OuterRef('pk')
)
pending_refund = OrderRefund.objects.filter(
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
order=OuterRef('pk')
)
qs = qs.annotate(
payment_sum=Subquery(payment_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10)),
refund_sum=Subquery(refund_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10)),
has_external_refund=Exists(external_refund),
has_pending_refund=Exists(pending_refund),
).annotate(
pending_sum_t=F('total') - Coalesce(F('payment_sum'), 0) + Coalesce(F('refund_sum'), 0),
pending_sum_rc=-1 * Coalesce(F('payment_sum'), 0) + Coalesce(F('refund_sum'), 0),
).annotate(
is_overpaid=Case(
When(~Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_t__lt=0),
then=Value('1')),
When(Q(status__in=(Order.STATUS_REFUNDED, Order.STATUS_CANCELED)) & Q(pending_sum_rc__lt=0),
then=Value('1')),
default=Value('0'),
output_field=models.IntegerField()
),
is_pending_with_full_payment=Case(
When(Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
& Q(require_approval=False),
then=Value('1')),
default=Value('0'),
output_field=models.IntegerField()
),
is_underpaid=Case(
When(Q(status=Order.STATUS_PAID) & Q(pending_sum_t__gt=0),
then=Value('1')),
default=Value('0'),
output_field=models.IntegerField()
)
)
return qs
@property
def full_code(self):
"""
@@ -278,48 +208,12 @@ class Order(LoggedModel):
def changable(self):
return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
def save(self, **kwargs):
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
def save(self, *args, **kwargs):
if not self.code:
self.assign_code()
if not self.datetime:
self.datetime = now()
if not self.expires:
self.set_expires()
super().save(**kwargs)
def touch(self):
self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
self.expires = exp_by_date
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents and subevents:
term_last = min([
term_last.datetime(se).date()
for se in subevents
])
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
super().save(*args, **kwargs)
@cached_property
def tax_total(self):
@@ -444,10 +338,7 @@ class Order(LoggedModel):
"payment settings is over."),
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
"payments should be accepted in the payment settings."),
'require_approval': _('This order is not yet approved by the event organizer.')
}
if self.require_approval:
return error_messages['require_approval']
term_last = self.payment_term_last
if term_last:
if now() > term_last:
@@ -495,8 +386,7 @@ class Order(LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None):
user: User=None, headers: dict=None, sender: str=None, invoices: list=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -521,7 +411,7 @@ class Order(LoggedModel):
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)
email_content = render_mail(template, context)[0]
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
@@ -533,7 +423,6 @@ class Order(LoggedModel):
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
@@ -571,14 +460,14 @@ class QuestionAnswer(models.Model):
"""
orderposition = models.ForeignKey(
'OrderPosition', null=True, blank=True,
related_name='answers', on_delete=models.CASCADE
related_name='answers'
)
cartposition = models.ForeignKey(
'CartPosition', null=True, blank=True,
related_name='answers', on_delete=models.CASCADE
related_name='answers'
)
question = models.ForeignKey(
Question, related_name='answers', on_delete=models.CASCADE
Question, related_name='answers'
)
options = models.ManyToManyField(
QuestionOption, related_name='answers', blank=True
@@ -658,15 +547,8 @@ class QuestionAnswer(models.Model):
def save(self, *args, **kwargs):
if self.orderposition and self.cartposition:
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
if self.orderposition:
self.orderposition.order.touch()
super().save(*args, **kwargs)
def delete(self, **kwargs):
if self.orderposition:
self.orderposition.order.touch()
super().delete(**kwargs)
class AbstractPosition(models.Model):
"""
@@ -726,7 +608,7 @@ class AbstractPosition(models.Model):
help_text=_("Empty, if this product is not an admission ticket")
)
voucher = models.ForeignKey(
'Voucher', null=True, blank=True, on_delete=models.CASCADE
'Voucher', null=True, blank=True
)
addon_to = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
@@ -783,441 +665,10 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
class OrderPayment(models.Model):
"""
Represents a payment or payment attempt for an order.
:param id: A globally unique ID for this payment
:type id:
:param local_id: An ID of this payment, counting from one for every order independently.
:type local_id: int
:param state: The state of the payment, one of ``created``, ``pending``, ``confirmed``, ``failed``,
``canceled``, or ``refunded``.
:type state: str
:param amount: The payment amount
:type amount: Decimal
:param order: The order that is paid
:type order: Order
:param created: The creation time of this record
:type created: datetime
:param payment_date: The completion time of this payment
:type payment_date: datetime
:param provider: The payment provider in use
:type provider: str
:param info: Provider-specific meta information (in JSON format)
:type info: str
:param fee: The ``OrderFee`` object used to track the fee for this order.
:type fee: pretix.base.models.OrderFee
"""
PAYMENT_STATE_CREATED = 'created'
PAYMENT_STATE_PENDING = 'pending'
PAYMENT_STATE_CONFIRMED = 'confirmed'
PAYMENT_STATE_FAILED = 'failed'
PAYMENT_STATE_CANCELED = 'canceled'
PAYMENT_STATE_REFUNDED = 'refunded'
PAYMENT_STATES = (
(PAYMENT_STATE_CREATED, pgettext_lazy('payment_state', 'created')),
(PAYMENT_STATE_PENDING, pgettext_lazy('payment_state', 'pending')),
(PAYMENT_STATE_CONFIRMED, pgettext_lazy('payment_state', 'confirmed')),
(PAYMENT_STATE_CANCELED, pgettext_lazy('payment_state', 'canceled')),
(PAYMENT_STATE_FAILED, pgettext_lazy('payment_state', 'failed')),
(PAYMENT_STATE_REFUNDED, pgettext_lazy('payment_state', 'refunded')),
)
local_id = models.PositiveIntegerField()
state = models.CharField(
max_length=190, choices=PAYMENT_STATES
)
amount = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Amount")
)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='payments',
on_delete=models.PROTECT
)
created = models.DateTimeField(
auto_now_add=True
)
payment_date = models.DateTimeField(
null=True, blank=True
)
provider = models.CharField(
null=True, blank=True,
max_length=255,
verbose_name=_("Payment provider")
)
info = models.TextField(
verbose_name=_("Payment information"),
null=True, blank=True
)
fee = models.ForeignKey(
'OrderFee',
null=True, blank=True, related_name='payments', on_delete=models.SET_NULL
)
migrated = models.BooleanField(default=False)
class Meta:
ordering = ('local_id',)
@property
def info_data(self):
"""
This property allows convenient access to the data stored in the ``info``
attribute by automatically encoding and decoding the content as JSON.
"""
return json.loads(self.info) if self.info else {}
@info_data.setter
def info_data(self, d):
self.info = json.dumps(d)
@cached_property
def payment_provider(self):
"""
Cached access to an instance of the payment provider in use.
"""
return self.order.event.get_payment_providers().get(self.provider)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required
:param count_waitinglist: Whether, when calculating quota, people on the waiting list should be taken into
consideration (default: ``True``).
:type count_waitinglist: boolean
:param force: Whether this payment should be marked as paid even if no remaining
quota is available (default: ``False``).
:type force: boolean
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
:type send_mail: boolean
:param user: The user who performed the change
:param auth: The API auth token that performed the change
:param mail_text: Additional text to be included in the email
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.signals import order_paid
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
self.state = self.PAYMENT_STATE_CONFIRMED
self.payment_date = now()
self.save()
self.order.log_action('pretix.event.order.payment.confirmed', {
'local_id': self.local_id,
'provider': self.provider,
}, user=user, auth=auth)
if self.order.status == Order.STATUS_PAID:
return
payment_sum = self.order.payments.filter(
state__in=(self.PAYMENT_STATE_CONFIRMED, self.PAYMENT_STATE_REFUNDED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
refund_sum = self.order.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
if payment_sum - refund_sum < self.order.total:
return
with self.order.event.lock():
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
if not force and can_be_paid is not True:
raise Quota.QuotaExceededException(can_be_paid)
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.log_action('pretix.event.order.paid', {
'provider': self.provider,
'info': self.info,
'date': self.payment_date,
'force': force
}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
invoice = None
if invoice_qualified(self.order):
invoices = self.order.invoices.filter(is_cancellation=False).count()
cancellations = self.order.invoices.filter(is_cancellation=True).count()
gen_invoice = (
(invoices == 0 and self.order.event.settings.get('invoice_generate') in ('True', 'paid')) or
0 < invoices <= cancellations
)
if gen_invoice:
invoice = generate_invoice(
self.order,
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
if send_mail:
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else []
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property
def refunded_amount(self):
"""
The sum of all refund amounts in ``done``, ``transit``, or ``created`` states associated
with this payment.
"""
return self.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
@property
def full_id(self):
"""
The full human-readable ID of this payment, constructed by the order code and the ``local_id``
field with ``-P-`` in between.
:return:
"""
return '{}-P-{}'.format(self.order.code, self.local_id)
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
super().save(*args, **kwargs)
def create_external_refund(self, amount=None, execution_date=None, info='{}'):
"""
This should be called to create an OrderRefund object when a refund has triggered
by an external source, e.g. when a credit card payment has been refunded by the
credit card provider.
:param amount: Amount to refund. If not given, the full payment amount will be used.
:type amount: Decimal
:param execution_date: Date of the refund. Defaults to the current time.
:type execution_date: datetime
:param info: Additional information, defaults to ``"{}"``.
:type info: str
:return: OrderRefund
"""
r = self.order.refunds.create(
state=OrderRefund.REFUND_STATE_EXTERNAL,
source=OrderRefund.REFUND_SOURCE_EXTERNAL,
amount=amount if amount is not None else self.amount,
order=self.order,
payment=self,
execution_date=execution_date or now(),
provider=self.provider,
info=info
)
self.order.log_action('pretix.event.order.refund.created.externally', {
'local_id': r.local_id,
'provider': r.provider,
})
return r
class OrderRefund(models.Model):
"""
Represents a refund or refund attempt for an order.
:param id: A globally unique ID for this refund
:type id:
:param local_id: An ID of this refund, counting from one for every order independently.
:type local_id: int
:param state: The state of the refund, one of ``created``, ``transit``, ``external``, ``canceled``,
``failed``, or ``done``.
:type state: str
:param source: How this refund was started, one of ``buyer``, ``admin``, or ``external``.
:param amount: The refund amount
:type amount: Decimal
:param order: The order that is refunded
:type order: Order
:param created: The creation time of this record
:type created: datetime
:param execution_date: The completion time of this refund
:type execution_date: datetime
:param provider: The payment provider in use
:type provider: str
:param info: Provider-specific meta information in JSON format
:type info: dict
"""
# REFUND_STATE_REQUESTED = 'requested'
# REFUND_STATE_APPROVED = 'approved'
REFUND_STATE_EXTERNAL = 'external'
REFUND_STATE_TRANSIT = 'transit'
REFUND_STATE_DONE = 'done'
# REFUND_STATE_REJECTED = 'rejected'
REFUND_STATE_CANCELED = 'canceled'
REFUND_STATE_CREATED = 'created'
REFUND_STATE_FAILED = 'failed'
REFUND_STATES = (
# (REFUND_STATE_REQUESTED, pgettext_lazy('refund_state', 'requested')),
# (REFUND_STATE_APPROVED, pgettext_lazy('refund_state', 'approved')),
(REFUND_STATE_EXTERNAL, pgettext_lazy('refund_state', 'started externally')),
(REFUND_STATE_CREATED, pgettext_lazy('refund_state', 'created')),
(REFUND_STATE_TRANSIT, pgettext_lazy('refund_state', 'in transit')),
(REFUND_STATE_DONE, pgettext_lazy('refund_state', 'done')),
(REFUND_STATE_FAILED, pgettext_lazy('refund_state', 'failed')),
# (REFUND_STATE_REJECTED, pgettext_lazy('refund_state', 'rejected')),
(REFUND_STATE_CANCELED, pgettext_lazy('refund_state', 'canceled')),
)
REFUND_SOURCE_BUYER = 'buyer'
REFUND_SOURCE_ADMIN = 'admin'
REFUND_SOURCE_EXTERNAL = 'external'
REFUND_SOURCES = (
(REFUND_SOURCE_ADMIN, pgettext_lazy('refund_source', 'Organizer')),
(REFUND_SOURCE_BUYER, pgettext_lazy('refund_source', 'Customer')),
(REFUND_SOURCE_EXTERNAL, pgettext_lazy('refund_source', 'External')),
)
local_id = models.PositiveIntegerField()
state = models.CharField(
max_length=190, choices=REFUND_STATES
)
source = models.CharField(
max_length=190, choices=REFUND_SOURCES
)
amount = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Amount")
)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='refunds',
on_delete=models.PROTECT
)
payment = models.ForeignKey(
OrderPayment,
null=True, blank=True,
related_name='refunds',
on_delete=models.PROTECT
)
created = models.DateTimeField(
auto_now_add=True
)
execution_date = models.DateTimeField(
null=True, blank=True
)
provider = models.CharField(
null=True, blank=True,
max_length=255,
verbose_name=_("Payment provider")
)
info = models.TextField(
verbose_name=_("Payment information"),
null=True, blank=True
)
class Meta:
ordering = ('local_id',)
@property
def info_data(self):
"""
This property allows convenient access to the data stored in the ``info``
attribute by automatically encoding and decoding the content as JSON.
"""
return json.loads(self.info) if self.info else {}
@info_data.setter
def info_data(self, d):
self.info = json.dumps(d)
@cached_property
def payment_provider(self):
"""
Cached access to an instance of the payment provider in use.
"""
return self.order.event.get_payment_providers().get(self.provider)
@transaction.atomic
def done(self, user=None, auth=None):
"""
Marks the refund as complete. This does not modify the state of the order.
:param user: The user who performed the change
:param user: The API auth token that performed the change
"""
self.state = self.REFUND_STATE_DONE
self.execution_date = self.execution_date or now()
self.save()
self.order.log_action('pretix.event.order.refund.done', {
'local_id': self.local_id,
'provider': self.provider,
}, user=user, auth=auth)
if self.payment and self.payment.refunded_amount >= self.payment.amount:
self.payment.state = OrderPayment.PAYMENT_STATE_REFUNDED
self.payment.save(update_fields=['state'])
@property
def full_id(self):
"""
The full human-readable ID of this refund, constructed by the order code and the ``local_id``
field with ``-R-`` in between.
:return:
"""
return '{}-R-{}'.format(self.order.code, self.local_id)
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
super().save(*args, **kwargs)
class OrderFee(models.Model):
"""
An OrderFee object represents a fee that is added to the order total independently of
An OrderFee objet represents a fee that is added to the order total independently of
the actual positions. This might for example be a payment or a shipping fee.
:param value: Gross price of this fee
:type value: Decimal
:param order: Order this fee is charged with
:type order: Order
:param fee_type: The type of the fee, currently ``payment``, ``shipping``, ``service``, ``giftcard``, or ``other``.
:type fee_type: str
:param description: A human-readable description of the fee
:type description: str
:param internal_type: An internal string to group fees by, e.g. the identifier string of a payment provider
:type internal_type: str
:param tax_rate: The tax rate applied to this fee
:type tax_rate: Decimal
:param tax_rule: The tax rule applied to this fee
:type tax_rule: TaxRule
:param tax_value: The tax amount included in the price
:type tax_value: Decimal
"""
FEE_TYPE_PAYMENT = "payment"
FEE_TYPE_SHIPPING = "shipping"
@@ -1300,13 +751,8 @@ class OrderFee(models.Model):
def save(self, *args, **kwargs):
if self.tax_rate is None:
self._calculate_tax()
self.order.touch()
return super().save(*args, **kwargs)
def delete(self, **kwargs):
self.order.touch()
super().delete(**kwargs)
class OrderPosition(AbstractPosition):
"""
@@ -1316,18 +762,6 @@ class OrderPosition(AbstractPosition):
:param order: The order this position is a part of
:type order: Order
:param positionid: A local ID of this position, counted for each order individually
:type positionid: int
:param tax_rate: The tax rate applied to this position
:type tax_rate: Decimal
:param tax_rule: The tax rule applied to this position
:type tax_rule: TaxRule
:param tax_value: The tax amount included in the price
:type tax_value: Decimal
:param secret: The secret used for ticket QR codes
:type secret: str
:param pseudonymization_id: The QR code content for lead scanning
:type pseudonymization_id: str
"""
positionid = models.PositiveIntegerField(default=1)
order = models.ForeignKey(
@@ -1350,11 +784,6 @@ class OrderPosition(AbstractPosition):
verbose_name=_('Tax value')
)
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
pseudonymization_id = models.CharField(
max_length=16,
unique=True,
db_index=True
)
class Meta:
verbose_name = _("Order position")
@@ -1432,28 +861,11 @@ class OrderPosition(AbstractPosition):
def save(self, *args, **kwargs):
if self.tax_rate is None:
self._calculate_tax()
self.order.touch()
if self.pk is None:
while OrderPosition.objects.filter(secret=self.secret).exists():
self.secret = generate_position_secret()
if not self.pseudonymization_id:
self.assign_pseudonymization_id()
return super().save(*args, **kwargs)
def assign_pseudonymization_id(self):
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
# might include OCR'd handwritten text
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=10, allowed_chars=charset)
if not OrderPosition.objects.filter(pseudonymization_id=code).exists():
self.pseudonymization_id = code
return
class CartPosition(AbstractPosition):
"""
@@ -1471,8 +883,7 @@ class CartPosition(AbstractPosition):
"""
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
on_delete=models.CASCADE
verbose_name=_("Event")
)
cart_id = models.CharField(
max_length=255, null=True, blank=True, db_index=True,
@@ -1516,7 +927,7 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address')
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
@@ -1534,11 +945,6 @@ class InvoiceAddress(models.Model):
blank=True
)
def save(self, **kwargs):
if self.order:
self.order.touch()
super().save(**kwargs)
def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)

View File

@@ -42,7 +42,6 @@ class Organizer(LoggedModel):
OrganizerSlugBlacklistValidator()
],
verbose_name=_("Short form"),
unique=True
)
class Meta:
@@ -138,9 +137,7 @@ class Team(LoggedModel):
)
can_change_organizer_settings = models.BooleanField(
default=False,
verbose_name=_("Can change organizer settings"),
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")
)
can_change_event_settings = models.BooleanField(

View File

@@ -60,7 +60,7 @@ EU_CURRENCIES = {
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
event = models.ForeignKey('Event', related_name='tax_rules')
name = I18nCharField(
verbose_name=_('Name'),
help_text=_('Should be short, e.g. "VAT"'),
@@ -160,19 +160,18 @@ class TaxRule(LoggedModel):
def get_matching_rule(self, invoice_address):
rules = json.loads(self.custom_rules)
if invoice_address:
for r in rules:
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
continue
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
continue
return r
for r in rules:
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
continue
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
continue
return r
return {'action': 'vat'}
def is_reverse_charge(self, invoice_address):

View File

@@ -137,14 +137,14 @@ class Voucher(LoggedModel):
item = models.ForeignKey(
Item, related_name='vouchers',
verbose_name=_("Product"),
null=True, blank=True, on_delete=models.CASCADE,
null=True, blank=True,
help_text=_(
"This product is added to the user's cart if the voucher is redeemed."
)
)
variation = models.ForeignKey(
ItemVariation, related_name='vouchers',
null=True, blank=True, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name=_("Product variation"),
help_text=_(
"This variation of the product select above is being used."
@@ -152,7 +152,7 @@ class Voucher(LoggedModel):
)
quota = models.ForeignKey(
Quota, related_name='quota',
null=True, blank=True, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name=_("Quota"),
help_text=_(
"If enabled, the voucher is valid for any product affected by this quota."

View File

@@ -42,12 +42,10 @@ class WaitingListEntry(LoggedModel):
voucher = models.ForeignKey(
'Voucher',
verbose_name=_("Assigned voucher"),
null=True, blank=True,
related_name='waitinglistentries',
on_delete=models.CASCADE
null=True, blank=True
)
item = models.ForeignKey(
Item, related_name='waitinglistentries', on_delete=models.CASCADE,
Item, related_name='waitinglistentries',
verbose_name=_("Product"),
help_text=_(
"The product the user waits for."
@@ -55,7 +53,7 @@ class WaitingListEntry(LoggedModel):
)
variation = models.ForeignKey(
ItemVariation, related_name='waitinglistentries',
null=True, blank=True, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name=_("Product variation"),
help_text=_(
"The variation of the product selected above."
@@ -65,12 +63,11 @@ class WaitingListEntry(LoggedModel):
max_length=190,
default='en'
)
priority = models.IntegerField(default=0)
class Meta:
verbose_name = _("Waiting list entry")
verbose_name_plural = _("Waiting list entries")
ordering = ('-priority', 'created')
ordering = ['created']
def __str__(self):
return '%s waits for %s' % (str(self.email), str(self.item))
@@ -80,7 +77,7 @@ class WaitingListEntry(LoggedModel):
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
def send_voucher(self, quota_cache=None, user=None, auth=None):
def send_voucher(self, quota_cache=None, user=None, api_token=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
if self.variation
@@ -117,8 +114,8 @@ class WaitingListEntry(LoggedModel):
'email': self.email,
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth)
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
}, user=user, api_token=api_token)
self.log_action('pretix.waitinglist.voucher', user=user, api_token=api_token)
self.voucher = v
self.save()

View File

@@ -229,12 +229,6 @@ def register_default_notification_types(sender, **kwargs):
_('Order changed'),
_('Order {order.code} has been changed.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.refund.created.externally',
_('External refund of payment'),
_('An external refund for {order.code} has occurred.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.refunded',

View File

@@ -1,4 +1,3 @@
import json
import logging
from collections import OrderedDict
from decimal import ROUND_HALF_UP, Decimal
@@ -7,25 +6,20 @@ from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib import messages
from django.dispatch import receiver
from django.forms import Form
from django.http import HttpRequest
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextarea
from i18nfield.strings import LazyI18nString
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, Order, OrderPayment, OrderRefund, Quota,
)
from pretix.base.models import CartPosition, Event, Order, Quota
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import get_or_create_cart_id
@@ -136,16 +130,6 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
@property
def abort_pending_allowed(self) -> bool:
"""
Whether or not a user can abort a payment in pending start to switch to another
payment method. This returns ``False`` by default which is no guarantee that
aborting a pending payment can never happen, it just hides the frontend button
to avoid users accidentally committing double payments.
"""
return False
@property
def settings_form_fields(self) -> dict:
"""
@@ -201,28 +185,6 @@ class BasePaymentProvider:
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
('_total_min',
forms.DecimalField(
label=_('Minimum order total'),
help_text=_('This payment will be available only if the order total is equal to or exceeds the given '
'value. The order total for this purpose may be computed without taking the fees imposed '
'by this payment method into account.'),
localize=True,
required=False,
decimal_places=places,
widget=DecimalTextInput(places=places)
)),
('_total_max',
forms.DecimalField(
label=_('Maximum order total'),
help_text=_('This payment will be available only if the order total is equal to or below the given '
'value. The order total for this purpose may be computed without taking the fees imposed '
'by this payment method into account.'),
localize=True,
required=False,
decimal_places=places,
widget=DecimalTextInput(places=places)
)),
('_fee_abs',
forms.DecimalField(
label=_('Additional fee'),
@@ -342,40 +304,20 @@ class BasePaymentProvider:
return True
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
def is_allowed(self, request: HttpRequest) -> bool:
"""
You can use this method to disable this payment provider for certain groups
of users, products or other criteria. If this method returns ``False``, the
user will not be able to select this payment method. This will only be called
during checkout, not on retrying.
The default implementation checks for the _availability_date setting to be either unset or in the future
and for the _total_max and _total_min requirements to be met.
:param total: The total value without the payment method fee, after taxes.
.. versionchanged:: 1.17.0
The ``total`` parameter has been added. For backwards compatibility, this method is called again
without this parameter if it raises a ``TypeError`` on first try.
The default implementation checks for the _availability_date setting to be either unset or in the future.
"""
timing = self._is_still_available(cart_id=get_or_create_cart_id(request))
pricing = True
return self._is_still_available(cart_id=get_or_create_cart_id(request))
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
raise ImproperlyConfigured('This payment provider does not support maximum or minimum amounts.')
if self.settings._total_max is not None:
pricing = pricing and total <= Decimal(self.settings._total_max)
if self.settings._total_min is not None:
pricing = pricing and total >= Decimal(self.settings._total_min)
return timing and pricing
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
def payment_form_render(self, request: HttpRequest) -> str:
"""
When the user selects this provider as their preferred payment method,
When the user selects this provider as his preferred payment method,
they will be shown the HTML you return from this method.
The default implementation will call :py:meth:`checkout_form`
@@ -390,8 +332,8 @@ class BasePaymentProvider:
def checkout_confirm_render(self, request) -> str:
"""
If the user has successfully filled in their payment data, they will be redirected
to a confirmation page which lists all details of their order for a final review.
If the user has successfully filled in his payment data, they will be redirected
to a confirmation page which lists all details of his order for a final review.
This method should return the HTML which should be displayed inside the
'Payment' box on this page.
@@ -400,19 +342,11 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def payment_pending_render(self, request: HttpRequest, payment: OrderPayment) -> str:
"""
Render customer-facing instructions on how to proceed with a pending payment
:return: HTML
"""
return ""
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
"""
Will be called after the user selects this provider as their payment method.
Will be called after the user selects this provider as his payment method.
If you provided a form to the user to enter payment data, this method should
at least store the user's input into their session.
at least store the user's input into his session.
This method should return ``False`` if the user's input was invalid, ``True``
if the input was valid and the frontend should continue with default behavior
@@ -427,7 +361,7 @@ class BasePaymentProvider:
If your payment method requires you to redirect the user to an external provider,
this might be the place to do so.
.. IMPORTANT:: If this is called, the user has not yet confirmed their order.
.. IMPORTANT:: If this is called, the user has not yet confirmed his or her order.
You may NOT do anything which actually moves money.
:param cart: This dictionary contains at least the following keys:
@@ -462,29 +396,26 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
def payment_perform(self, request: HttpRequest, order: Order) -> str:
"""
After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable.
You will be passed an :py:class:`pretix.base.models.OrderPayment` object that contains
the amount of money that should be paid.
If you need any special behavior, you can return a string
If you need any special behavior, you can return a string
containing the URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page.
If the payment is completed, you should call ``payment.confirm()``. Please note that ``this`` might
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
some of the items are sold out. You should use the exception message to display a meaningful error
to the user.
If the payment is completed, you should call ``pretix.base.services.orders.mark_order_paid(order, provider, info)``
with ``provider`` being your :py:attr:`identifier` and ``info`` being any string
you might want to store for later usage. Please note that ``mark_order_paid`` might
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this
order is over and some of the items are sold out. You should use the exception message
to display a meaningful error to the user.
The default implementation just returns ``None`` and therefore leaves the
order unpaid. The user will be redirected to the order's detail page by default.
On errors, you should raise a ``PaymentException``.
:param order: The order object
:param payment: An ``OrderPayment`` instance
"""
return None
@@ -498,6 +429,19 @@ class BasePaymentProvider:
"""
return ""
def order_pending_render(self, request: HttpRequest, order: Order) -> str:
"""
If the user visits a detail page of an order which has not yet been paid but
this payment method was selected during checkout, this method will be called
to provide HTML content for the 'payment' box on the page.
It should contain instructions on how to continue with the payment process,
either in form of text or buttons/links/etc.
:param order: The order object
"""
raise NotImplementedError() # NOQA
def order_change_allowed(self, order: Order) -> bool:
"""
Will be called to check whether it is allowed to change the payment method of
@@ -507,16 +451,33 @@ class BasePaymentProvider:
:param order: The order object
"""
ps = order.pending_sum
if self.settings._total_max is not None and ps > Decimal(self.settings._total_max):
return False
if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
return False
return self._is_still_available(order=order)
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
def order_can_retry(self, order: Order) -> bool:
"""
Will be called if the user views the detail page of an unpaid order to determine
whether the user should be presented with an option to retry the payment. The default
implementation always returns False.
If you want to enable retrials for your payment method, the best is to just return
``self._is_still_available()`` from this method to disable it as soon as the method
gets disabled or the methods end date is reached.
The retry workflow is also used if a user switches to this payment method for an existing
order!
:param order: The order object
"""
return False
def retry_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
"""
Deprecated, use order_prepare instead
"""
raise DeprecationWarning('retry_prepare is deprecated, use order_prepare instead')
return self.order_prepare(request, order)
def order_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
"""
Will be called if the user retries to pay an unpaid order (after the user filled in
e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
@@ -537,9 +498,22 @@ class BasePaymentProvider:
else:
return False
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
def order_paid_render(self, request: HttpRequest, order: Order) -> str:
"""
Will be called if the *event administrator* views the details of a payment.
Will be called if the user views the detail page of a paid order which is
associated with this payment provider.
It should return HTML code which should be displayed to the user or None,
if there is nothing to say (like the default implementation does).
:param order: The order object
"""
return None
def order_control_render(self, request: HttpRequest, order: Order) -> str:
"""
Will be called if the *event administrator* views the detail page of an order
which is associated with this payment provider.
It should return HTML code containing information regarding the current payment
status and, if applicable, next steps.
@@ -548,44 +522,62 @@ class BasePaymentProvider:
:param order: The order object
"""
return ''
return _('Payment provider: %s' % self.verbose_name)
def payment_refund_supported(self, payment: OrderPayment) -> bool:
def order_control_refund_render(self, order: Order, request: HttpRequest=None) -> str:
"""
Will be called to check if the provider supports automatic refunding for this
payment.
"""
return False
Will be called if the event administrator clicks an order's 'refund' button.
This can be used to display information *before* the order is being refunded.
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
"""
Will be called to check if the provider supports automatic partial refunding for this
payment.
"""
return False
It should return HTML code which should be displayed to the user. It should
contain information about to which extend the money will be refunded
automatically.
def execute_refund(self, refund: OrderRefund):
"""
Will be called to execute an refund. Note that refunds have an amount property and can be partial.
:param order: The order object
:param request: The HTTP request
This should transfer the money back (if possible).
On success, you should call ``refund.done()``.
On failure, you should raise a PaymentException.
"""
raise PaymentException(_('Automatic refunds are not supported by this payment provider.'))
.. versionchanged:: 1.6
def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
The parameter ``request`` has been added.
"""
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
'please transfer the money back manually.')
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
"""
Will be called if the event administrator confirms the refund.
This should transfer the money back (if possible). You can return the URL the
user should be redirected to if you need special behavior or None to continue
with default behavior.
On failure, you should use Django's message framework to display an error message
to the user.
The default implementation sets the Order's state to refunded and shows a success
message.
:param request: The HTTP request
:param order: The order object
"""
from pretix.base.services.orders import mark_order_refunded
mark_order_refunded(order, user=request.user)
messages.success(request, _('The order has been marked as refunded. Please transfer the money '
'back to the buyer manually.'))
def shred_payment_info(self, order: Order):
"""
When personal data is removed from an event, this method is called to scrub payment-related data
from a payment or refund. By default, it removes all info from the ``info`` attribute. You can override
from an order. By default, it removes all info from the ``payment_info`` attribute. You can override
this behavior if you want to retain attributes that are not personal data on their own, i.e. a
reference to a transaction in an external system. You can also override this to scrub more data, e.g.
data from external sources that is saved in LogEntry objects or other places.
:param order: An order
"""
obj.info = '{}'
obj.save(update_fields=['info'])
order.payment_info = None
order.save(update_fields=['payment_info'])
class PaymentException(Exception):
@@ -593,13 +585,25 @@ class PaymentException(Exception):
class FreeOrderProvider(BasePaymentProvider):
is_implicit = True
is_enabled = True
identifier = "free"
@property
def is_implicit(self) -> bool:
return True
@property
def is_enabled(self) -> bool:
return True
@property
def identifier(self) -> str:
return "free"
def checkout_confirm_render(self, request: HttpRequest) -> str:
return _("No payment is required as this order only includes products which are free of charge.")
def order_pending_render(self, request: HttpRequest, order: Order) -> str:
pass
def payment_is_valid_session(self, request: HttpRequest) -> bool:
return True
@@ -607,9 +611,10 @@ class FreeOrderProvider(BasePaymentProvider):
def verbose_name(self) -> str:
return _("Free of charge")
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
def payment_perform(self, request: HttpRequest, order: Order):
from pretix.base.services.orders import mark_order_paid
try:
payment.confirm(send_mail=False)
mark_order_paid(order, 'free', send_mail=False)
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
@@ -617,7 +622,32 @@ class FreeOrderProvider(BasePaymentProvider):
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
def order_control_refund_render(self, order: Order) -> str:
return ''
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
"""
Will be called if the event administrator confirms the refund.
This should transfer the money back (if possible). You can return the URL the
user should be redirected to if you need special behavior or None to continue
with default behavior.
On failure, you should use Django's message framework to display an error message
to the user.
The default implementation sets the Order's state to refunded and shows a success
message.
:param request: The HTTP request
:param order: The order object
"""
from pretix.base.services.orders import mark_order_refunded
mark_order_refunded(order, user=request.user)
messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool:
from .services.cart import get_fees
total = get_cart_total(request)
@@ -628,155 +658,6 @@ class FreeOrderProvider(BasePaymentProvider):
return False
class BoxOfficeProvider(BasePaymentProvider):
is_implicit = True
is_enabled = True
identifier = "boxoffice"
verbose_name = _("Box office")
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
try:
payment.confirm(send_mail=False)
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
@property
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
def order_change_allowed(self, order: Order) -> bool:
return False
class ManualPayment(BasePaymentProvider):
identifier = 'manual'
verbose_name = _('Manual payment')
@property
def is_implicit(self):
return 'pretix.plugins.manualpayment' not in self.event.plugins
def is_allowed(self, request: HttpRequest, total: Decimal=None):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().is_allowed(request, total)
def order_change_allowed(self, order: Order):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order)
@property
def public_name(self):
return str(self.settings.get('public_name', as_type=LazyI18nString))
@property
def settings_form_fields(self):
d = OrderedDict(
[
('public_name', I18nFormField(
label=_('Payment method name'),
widget=I18nTextInput,
)),
('checkout_description', I18nFormField(
label=_('Payment process description during checkout'),
help_text=_('This text will be shown during checkout when the user selects this payment method. '
'It should give a short explanation on this payment method.'),
widget=I18nTextarea,
)),
('email_instructions', I18nFormField(
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
('pending_description', I18nFormField(
label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
] + list(super().settings_form_fields.items())
)
d.move_to_end('_enabled', last=False)
return d
def payment_form_render(self, request) -> str:
return rich_text(
str(self.settings.get('checkout_description', as_type=LazyI18nString))
)
def checkout_prepare(self, request, total):
return True
def payment_is_valid_session(self, request):
return True
def checkout_confirm_render(self, request):
return self.payment_form_render(request)
def format_map(self, order):
return {
'order': order.code,
'total': order.total,
'currency': self.event.currency,
'total_with_currency': money_filter(order.total, self.event.currency)
}
def order_pending_mail_render(self, order) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
return msg
def order_pending_render(self, request, order) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(order))
)
class OffsettingProvider(BasePaymentProvider):
is_enabled = True
identifier = "offsetting"
verbose_name = _("Offsetting")
is_implicit = True
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
try:
payment.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
def execute_refund(self, refund: OrderRefund):
code = refund.info_data['orders'][0]
try:
order = Order.objects.get(code=code, event__organizer=self.event.organizer)
except Order.DoesNotExist:
raise PaymentException(_('You entered an order that could not be found.'))
p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_PENDING,
amount=refund.amount,
payment_date=now(),
provider='offsetting',
info=json.dumps({'orders': [refund.order.code]})
)
p.confirm()
@property
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
def order_change_allowed(self, order: Order) -> bool:
return False
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment]
return FreeOrderProvider

View File

@@ -24,7 +24,6 @@ from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -47,7 +46,7 @@ DEFAULT_VARIABLES = OrderedDict((
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.name)
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}),
("variation", {
"label": _("Variation name"),
@@ -63,8 +62,8 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Product name and variation"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: (
'{} - {}'.format(orderposition.item.name, orderposition.variation)
if orderposition.variation else str(orderposition.item.name)
'{} - {}'.format(orderposition.item, orderposition.variation)
if orderposition.variation else str(orderposition.item)
)
}),
("item_category", {
@@ -149,12 +148,12 @@ DEFAULT_VARIABLES = OrderedDict((
("invoice_name", {
"label": _("Invoice address: name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
}),
("invoice_company", {
"label": _("Invoice address: company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
}),
("addons", {
"label": _("List of Add-Ons"),
@@ -211,42 +210,17 @@ class Renderer:
if 'bolditalic' in styles:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
def _draw_poweredby(self, canvas: Canvas, op: OrderPosition, o: dict):
content = o.get('content', 'dark')
img = finders.find('pretixpresale/pdf/powered_by_pretix_{}.png'.format(content))
ir = ThumbnailingImageReader(img)
try:
width, height = ir.resize(None, float(o['size']) * mm, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
float(o['left']) * mm, float(o['bottom']) * mm,
width=width, height=height,
preserveAspectRatio=True, anchor='n',
mask='auto')
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
content = o.get('content', 'secret')
if content == 'secret':
content = op.secret
elif content == 'pseudonymization_id':
content = op.pseudonymization_id
reqs = float(o['size']) * mm
qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs)
qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
d = Drawing(reqs, reqs)
d.add(qrw)
qr_x = float(o['left']) * mm
qr_y = float(o['bottom']) * mm
renderPDF.draw(d, canvas, qr_x, qr_y)
def _get_ev(self, op, order):
return op.subevent or order.event
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
ev = self._get_ev(op, order)
ev = op.subevent or order.event
if not o['content']:
return '(error)'
if o['content'] == 'other':
@@ -301,8 +275,6 @@ class Renderer:
self._draw_barcodearea(canvas, op, o)
elif o['type'] == "textarea":
self._draw_textarea(canvas, op, order, o)
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
canvas.showPage()
def render_background(self, buffer, title=_('Ticket')):

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