forked from CGM_Public/pretix_original
Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ffe55453 | ||
|
|
ab865e716f | ||
|
|
0bf1832b23 | ||
|
|
650adb9235 | ||
|
|
e2d55fed0d | ||
|
|
aef751dbee | ||
|
|
cd084fe8d1 | ||
|
|
c68b6116a2 | ||
|
|
f0db879c9c | ||
|
|
07d8a3d765 | ||
|
|
e35e264d81 | ||
|
|
d537e6a869 | ||
|
|
d4dd1861a9 | ||
|
|
3019a31fbb | ||
|
|
303b9912ff | ||
|
|
0259b2e5b9 | ||
|
|
5c7e8029f4 | ||
|
|
08e3fd3141 | ||
|
|
30123fd6ff | ||
|
|
3955299983 | ||
|
|
b5d0df3ca7 | ||
|
|
22c65da9d1 | ||
|
|
578c1ecfaf | ||
|
|
d8d00a7e26 | ||
|
|
37f0f7a138 | ||
|
|
f61e9367ec | ||
|
|
3c3e59e932 | ||
|
|
29e22a0c6c | ||
|
|
0d1f424425 | ||
|
|
1c01e23867 | ||
|
|
f763a8694b | ||
|
|
675b853b29 | ||
|
|
2434bf14d5 | ||
|
|
70fbbfe2a0 | ||
|
|
e096898a05 | ||
|
|
3fbccf3f64 | ||
|
|
36585395f1 | ||
|
|
e4b0a1613f | ||
|
|
1192e474c5 | ||
|
|
e48ea99e48 | ||
|
|
072f2a0ee9 | ||
|
|
aecb536a34 | ||
|
|
a68686cb06 | ||
|
|
ba8cf3e01e | ||
|
|
b0c5189c4b | ||
|
|
d44eb67dec | ||
|
|
58d36b08e2 | ||
|
|
98906731e3 | ||
|
|
035a4b0928 | ||
|
|
85fbe666ea | ||
|
|
741d0bc686 | ||
|
|
ded539ce7a | ||
|
|
c53fd25d1c | ||
|
|
da32621c55 | ||
|
|
4ccf33af03 | ||
|
|
a5af7a70f3 | ||
|
|
16ab0d29d6 | ||
|
|
05ad9022c0 | ||
|
|
fef211b220 | ||
|
|
6aee1ee41f | ||
|
|
bab7f9b1f3 | ||
|
|
340e7afd06 | ||
|
|
cb83c9cff2 | ||
|
|
911a8fed06 | ||
|
|
eb8b43fe36 | ||
|
|
2a15dc57d8 | ||
|
|
67678e35bb | ||
|
|
2f00db8081 | ||
|
|
38fc826053 | ||
|
|
300578a44b | ||
|
|
dc2bcdcfbc | ||
|
|
7e18e89012 | ||
|
|
24f47722c0 | ||
|
|
04b679a4a7 | ||
|
|
f6713008aa | ||
|
|
15dc62855b | ||
|
|
4ed3df2b08 | ||
|
|
8a3eaae29c | ||
|
|
22edc016dd | ||
|
|
5205daae6d | ||
|
|
7ea79ebe56 | ||
|
|
3bfa8bd81e | ||
|
|
39abf63698 | ||
|
|
f68a6d1119 | ||
|
|
1a1a02d080 | ||
|
|
dacffc5f90 | ||
|
|
f2068b2663 | ||
|
|
989282ffbe | ||
|
|
e469b2e6ad | ||
|
|
8eaada992f | ||
|
|
f5dba45fa0 | ||
|
|
e72b5893c4 | ||
|
|
e78a176e9f | ||
|
|
8143999803 | ||
|
|
219c2c94e8 | ||
|
|
37f612801f | ||
|
|
0b12b7aa89 | ||
|
|
14da25bd9e | ||
|
|
3a713541a2 | ||
|
|
c7a547a875 | ||
|
|
e12caf186c | ||
|
|
1ee6e31538 | ||
|
|
083c94403b | ||
|
|
67121decbf | ||
|
|
fcd6bb1084 | ||
|
|
a81a4b895a | ||
|
|
c50c5177b8 | ||
|
|
30eefe57ef | ||
|
|
ce33cce5a9 | ||
|
|
d0dfde382c | ||
|
|
7fb2d0526e | ||
|
|
fb34467cba | ||
|
|
7e62cddb97 | ||
|
|
78b31149b5 | ||
|
|
817038563f | ||
|
|
56ca2305bd | ||
|
|
fc7bafe3d9 | ||
|
|
d622f38e1d | ||
|
|
139810c8a5 | ||
|
|
f8cc332ed7 | ||
|
|
db24bd4d78 | ||
|
|
d056013296 | ||
|
|
7e647f7085 | ||
|
|
322068b5e0 | ||
|
|
96247d5fa0 | ||
|
|
6b7338aff0 | ||
|
|
59d85cc218 | ||
|
|
7f90fdedf1 | ||
|
|
7723c956bc | ||
|
|
d0c10a8f72 | ||
|
|
c56dd52bd6 | ||
|
|
a7374f5bbd | ||
|
|
251d62f3c4 | ||
|
|
b8c041d0d6 | ||
|
|
dd42037f21 | ||
|
|
50575d45c1 | ||
|
|
7268c7fb70 | ||
|
|
83572960d5 | ||
|
|
39f22fa314 | ||
|
|
69ab5d8c2e | ||
|
|
58111465bc | ||
|
|
697e56962a | ||
|
|
7d6c45f689 | ||
|
|
faa34c9230 | ||
|
|
8a64c1815a | ||
|
|
f65f166ea6 | ||
|
|
f054e700a2 | ||
|
|
ff8b9e4e1a | ||
|
|
ed47e94a70 | ||
|
|
93090f3481 | ||
|
|
fa211574a2 | ||
|
|
9a18eda404 | ||
|
|
fd3e941472 | ||
|
|
ad03980c2b | ||
|
|
a6af7a90cb | ||
|
|
ba170632a5 | ||
|
|
f8c536afd3 | ||
|
|
53b75e1ca2 | ||
|
|
a632b08664 | ||
|
|
954b7f6d9e | ||
|
|
8c0fb90420 | ||
|
|
5d697a3189 | ||
|
|
2c04a4daec | ||
|
|
f3c1296105 | ||
|
|
50c3f025e2 | ||
|
|
9b4a3bcbef | ||
|
|
d8486e8f90 | ||
|
|
fc731c3f58 | ||
|
|
a62fbd54d4 | ||
|
|
f81a7a397a | ||
|
|
8543cb7ece | ||
|
|
5015c9d8f3 | ||
|
|
2b4dede658 | ||
|
|
d2b15ae679 | ||
|
|
5ece8fd2f7 | ||
|
|
128203800c | ||
|
|
f0a1397eea | ||
|
|
ef42055de1 | ||
|
|
15aff6030c | ||
|
|
215a28fac5 | ||
|
|
e9e155201f | ||
|
|
21a457cadd | ||
|
|
8e7eab636f | ||
|
|
d7ce46c14a | ||
|
|
ee7f8940d0 | ||
|
|
cf1883a039 | ||
|
|
7ec614a691 | ||
|
|
e4fc49769f | ||
|
|
d97df9cddd | ||
|
|
33a254ce92 | ||
|
|
3ec0366c21 | ||
|
|
759db2d20e | ||
|
|
8f7c6521a9 | ||
|
|
725a7f21c4 | ||
|
|
e24611fde9 | ||
|
|
7a077095ee | ||
|
|
fade09ca8f | ||
|
|
9ab39904e8 | ||
|
|
fb5fa57fd6 | ||
|
|
9e84e78215 | ||
|
|
f5bf2ac4ca | ||
|
|
c50a8e7740 | ||
|
|
3ece911018 | ||
|
|
1011b67f0a | ||
|
|
497679284a | ||
|
|
263df3ac4d |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -6,7 +6,9 @@ src/static/datetimepicker/* linguist-vendored
|
||||
src/static/colorpicker/* linguist-vendored
|
||||
src/static/fileupload/* linguist-vendored
|
||||
src/static/vuejs/* linguist-vendored
|
||||
src/static/select2/* linguist-vendored
|
||||
src/static/charts/* linguist-vendored
|
||||
src/static/iframeresizer/* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ pypi:
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
|
||||
- cd src
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- python setup.py sdist upload
|
||||
- python setup.py bdist_wheel upload
|
||||
tags:
|
||||
|
||||
14
.travis.sh
14
.travis.sh
@@ -25,19 +25,27 @@ if [ "$1" == "doctests" ]; then
|
||||
cd doc
|
||||
make doctest
|
||||
fi
|
||||
if [ "$1" == "spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make spelling
|
||||
if [ -s _build/spelling/output.txt ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$1" == "tests" ]; then
|
||||
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 --rerun 5 tests
|
||||
py.test --reruns 5 tests
|
||||
fi
|
||||
if [ "$1" == "tests-cov" ]; then
|
||||
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 --rerun 5 tests && codecov
|
||||
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 -r src/requirements/py34.txt
|
||||
@@ -50,7 +58,7 @@ if [ "$1" == "plugins" ]; then
|
||||
cd pretix-cartshare
|
||||
python setup.py develop
|
||||
make
|
||||
py.test --rerun 5 tests
|
||||
py.test --reruns 5 tests
|
||||
popd
|
||||
|
||||
fi
|
||||
|
||||
@@ -36,5 +36,10 @@ matrix:
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
- python: 3.6
|
||||
env: JOB=spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
cd /pretix/src
|
||||
export DJANGO_SETTINGS_MODULE=production_settings
|
||||
export DATA_DIR=/data/
|
||||
export HOME=/pretix
|
||||
NUM_WORKERS=10
|
||||
|
||||
if [ ! -d /data/logs ]; then
|
||||
|
||||
@@ -175,3 +175,9 @@ pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
spelling:
|
||||
$(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
|
||||
@echo
|
||||
@echo "Spelling check finished, look at the results in " \
|
||||
"$(BUILDDIR)/spelling/output.txt."
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
.. _`config`:
|
||||
|
||||
.. spelling:: Galera
|
||||
|
||||
Configuration file
|
||||
==================
|
||||
|
||||
@@ -10,7 +12,7 @@ at the following locations. It will try to read the file from the specified path
|
||||
the following order. The file that is found *last* will override the settings from
|
||||
the files found before.
|
||||
|
||||
1. ``PREFIX_CONFIG_FILE`` environment variable
|
||||
1. ``PRETIX_CONFIG_FILE`` environment variable
|
||||
2. ``/etc/pretix/pretix.cfg``
|
||||
3. ``~/.pretix.cfg``
|
||||
4. ``pretix.cfg`` in the current working directory
|
||||
@@ -45,7 +47,7 @@ Example::
|
||||
|
||||
``datadir``
|
||||
The local path to a data directory that will be used for storing user uploads and similar
|
||||
data. Defaults to thea value of the environment variable ``DATA_DIR`` or ``data``.
|
||||
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
|
||||
|
||||
``plugins_default``
|
||||
A comma-separated list of plugins that are enabled by default for all new events.
|
||||
@@ -286,4 +288,4 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
|
||||
voucher_code=16
|
||||
|
||||
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
|
||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/configuration.html
|
||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
|
||||
|
||||
@@ -162,7 +162,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
-v /var/run/mysqld:/var/run/mysqld \
|
||||
pretix/standalone all
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.. highlight:: ini
|
||||
|
||||
.. spelling:: SQL
|
||||
|
||||
General remarks
|
||||
===============
|
||||
|
||||
|
||||
@@ -21,17 +21,26 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the check-in list
|
||||
name string The internal name of the check-in list
|
||||
all_products boolean If ``True``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
|
||||
all_products boolean If ``true``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
|
||||
limit_products list of integers List of item IDs to include in this list.
|
||||
subevent integer ID of the date inside an event series this list belongs to (or ``null``).
|
||||
position_count integer Number of tickets that match this list (read-only).
|
||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 1.11
|
||||
|
||||
The ``positions`` endpoints have been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The ``include_pending`` field has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -67,6 +76,7 @@ Endpoints
|
||||
"position_count": 456,
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
@@ -107,6 +117,7 @@ Endpoints
|
||||
"position_count": 456,
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -152,6 +163,7 @@ Endpoints
|
||||
"position_count": 0,
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -165,7 +177,7 @@ Endpoints
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(id)/
|
||||
|
||||
Update a check-in list. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field and the ``checkin_count`` and ``position_count``
|
||||
@@ -200,6 +212,7 @@ Endpoints
|
||||
"position_count": 42,
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -213,7 +226,7 @@ Endpoints
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||
|
||||
Delete a check-in list. Note that this also deletes the information on all checkins performed via this list.
|
||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -236,3 +249,163 @@ Endpoints
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
|
||||
Order position endpoints
|
||||
------------------------
|
||||
|
||||
.. 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
|
||||
the :ref:`order-position-resource`, with one important difference: the ``checkins`` value will only include
|
||||
check-ins for the selected list.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/positions/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
{
|
||||
"question": 12,
|
||||
"answer": "Foo",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"downloads": [
|
||||
{
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
|
||||
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
|
||||
``attendee_name,positionid``
|
||||
:query string order: Only return positions of the order with the given order code
|
||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
|
||||
products positions are shown if they refer to an attendee with the given name.
|
||||
:query string secret: Only return positions with the given ticket secret.
|
||||
:query bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
||||
checked in already on this list.
|
||||
:query integer subevent: Only return positions of the sub-event with the given ID
|
||||
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
|
||||
: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
|
||||
: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 check-in list does not exist.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)
|
||||
|
||||
Returns information on one order position, identified by its internal ID.
|
||||
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
|
||||
``checkins`` value will only include check-ins for the selected list.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/positions/ 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": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
{
|
||||
"question": 12,
|
||||
"answer": "Foo",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"downloads": [
|
||||
{
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
: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
|
||||
:param id: The ``id`` field of the order 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 order position or check-in list does not exist.
|
||||
|
||||
@@ -10,6 +10,8 @@ Resources and endpoints
|
||||
taxrules
|
||||
categories
|
||||
items
|
||||
item_variations
|
||||
item_add-ons
|
||||
questions
|
||||
quotas
|
||||
orders
|
||||
|
||||
@@ -221,5 +221,5 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting vor a few
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||
seconds.
|
||||
|
||||
246
doc/api/resources/item_add-ons.rst
Normal file
246
doc/api/resources/item_add-ons.rst
Normal file
@@ -0,0 +1,246 @@
|
||||
Item add-ons
|
||||
============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
With add-ons, you can specify products that can be bought as an addition to this specific product. For example, if you
|
||||
host a conference with a base conference ticket and a number of workshops, you could define the workshops as add-ons to
|
||||
the conference ticket. With this configuration, the workshops cannot be bought on their own but only in combination with
|
||||
a conference ticket. You can here specify categories of products that can be used as add-ons to this product. You can
|
||||
also specify the minimum and maximum number of add-ons of the given category that can or need to be chosen. The user can
|
||||
buy every add-on from the category at most once. If an add-on product has multiple variations, only one of them can be
|
||||
bought.
|
||||
The add-ons resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the add-on
|
||||
addon_category integer Internal ID of the item category the add-on can be
|
||||
chosen from.
|
||||
min_count integer The minimal number of add-ons that need to be chosen.
|
||||
max_count integer The maximal number of add-ons that can be chosen.
|
||||
position integer An integer, used for sorting
|
||||
price_included boolean Adding this add-on to the item is free
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/
|
||||
|
||||
Returns a list of all add-ons for a given item.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/addons/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 3,
|
||||
"addon_category": 1,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 0,
|
||||
"price_included": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"addon_category": 2,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"price_included": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/(id)/
|
||||
|
||||
Returns information on one add-on, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/addons/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": 3,
|
||||
"addon_category": 1,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:param id: The ``id`` field of the add-on 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.
|
||||
|
||||
.. http:post:: /api/v1/organizers/bigevents/events/sampleconf/items/1/addons/
|
||||
|
||||
Creates a new add-on
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"addon_category": 1,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"addon_category": 1,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a add-on for
|
||||
:param event: The ``slug`` field of the event to create a add-on for
|
||||
:param item: The ``id`` field of the item to create a add-on for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The add-on could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/addon/(id)/
|
||||
|
||||
Update an add-on. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/addons/3/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"addon_category": 1,
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param item: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the add-on to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The add-on could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/addons/(id)/
|
||||
|
||||
Delete an add-on.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/addons/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the add-on to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
258
doc/api/resources/item_variations.rst
Normal file
258
doc/api/resources/item_variations.rst
Normal file
@@ -0,0 +1,258 @@
|
||||
Item variations
|
||||
===============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Variations of items can be use for products (items) that are available in different sizes, colors or other variations
|
||||
of the same product.
|
||||
The addons resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID 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
|
||||
to the item's ``default_price`` (read-only).
|
||||
active boolean If ``False``, this variation will not be sold or shown.
|
||||
description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
position integer An integer, used for sorting
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/variations/
|
||||
|
||||
Returns a list of all variations for a given item.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/variations/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"value": {
|
||||
"en": "S"
|
||||
},
|
||||
"active": true,
|
||||
"description": {
|
||||
"en": "Test2"
|
||||
},
|
||||
"position": 0,
|
||||
"default_price": "223.00",
|
||||
"price": 223.0
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"value": {
|
||||
"en": "L"
|
||||
},
|
||||
"active": true,
|
||||
"description": {},
|
||||
"position": 1,
|
||||
"default_price": null,
|
||||
"price": 15.0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/variations/(id)/
|
||||
|
||||
Returns information on one variation, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/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": 3,
|
||||
"value": {
|
||||
"en": "Student"
|
||||
},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:param id: The ``id`` field of the variation 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.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/variations/
|
||||
|
||||
Creates a new variation
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a variation for
|
||||
:param event: The ``slug`` field of the event to create a variation for
|
||||
:param item: The ``id`` field of the item to create a variation for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The variation could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/variations/(id)/
|
||||
|
||||
Update a variation. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` and the ``price`` field.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"active": false,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": false,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the variation to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The variation could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/variations/(id)/
|
||||
|
||||
Delete a variation.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the variation to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
@@ -33,6 +33,7 @@ admission boolean ``True`` for it
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture string A product picture to be displayed in the shop
|
||||
(read-only).
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
@@ -53,10 +54,9 @@ 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.
|
||||
has_variations boolean Shows whether or not this item has variations
|
||||
(read-only).
|
||||
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.
|
||||
Can be empty. Only writable on POST.
|
||||
├ id integer Internal ID 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
|
||||
@@ -66,12 +66,14 @@ variations list of objects A list with one
|
||||
├ description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
└ position integer An integer, used for sorting
|
||||
addons list of objects Definition of add-ons that can be chosen for this item
|
||||
addons list of objects Definition of add-ons that can be chosen for this item.
|
||||
Only writable on POST.
|
||||
├ addon_category integer Internal ID of the item category the add-on can be
|
||||
chosen from.
|
||||
├ min_count integer The minimal number of add-ons that need to be chosen.
|
||||
├ max_count integer The maxima number of add-ons that can be chosen.
|
||||
├ max_count integer The maximal number of add-ons that can be chosen.
|
||||
└ position integer An integer, used for sorting
|
||||
└ price_included boolean Adding this add-on to the item is free
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
@@ -79,6 +81,20 @@ addons list of objects Definition of a
|
||||
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
|
||||
``checkin_attention`` has been added.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
The attribute ``price_included`` has been added to ``addons``.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Please note that an item either always has variations or never has. Once created with variations the item can never
|
||||
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
|
||||
one variation.
|
||||
|
||||
Also note that ``variations`` and ``addons`` are only supported on ``POST``. To update/delete variations and add-ons please
|
||||
use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT`` with nested
|
||||
``variations`` and/or ``addons``.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -239,3 +255,226 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
|
||||
|
||||
Creates a new item
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"default_price": "23.00",
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"default_price": "23.00",
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||
:param event: The ``slug`` field of the event to create an item for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The item could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
|
||||
|
||||
Update an item. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``has_variations``, ``variations`` and the ``addon`` field. If
|
||||
you need to update/delete variations or add-ons please use the nested dedicated endpoints.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"name": {"en": "Ticket"},
|
||||
"default_price": "25.00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Ticket"},
|
||||
"default_price": "25.00",
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
"default_price": "10.00",
|
||||
"price": "10.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"value": {"en": "Regular"},
|
||||
"default_price": null,
|
||||
"price": "23.00",
|
||||
"active": true,
|
||||
"description": null,
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The item could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/
|
||||
|
||||
Delete an item.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. spelling:: checkins
|
||||
|
||||
Orders
|
||||
======
|
||||
|
||||
@@ -32,6 +34,9 @@ payment_fee_tax_value money (string) Tax value inclu
|
||||
payment_fee_tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
@@ -79,13 +84,19 @@ downloads list of objects List of ticket
|
||||
|
||||
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
|
||||
deprecated in favour of the new ``fees`` attribute but will still be served and removed in 1.9.
|
||||
deprecated in favor of the new ``fees`` attribute but will still be served and removed in 1.9.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
|
||||
The attribute ``invoice_address.internal_reference`` has been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The field ``checkin_attention`` has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
-----------------------
|
||||
|
||||
@@ -110,6 +121,7 @@ secret string Secret code pri
|
||||
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``).
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
├ list integer Internal ID of the check-in list
|
||||
└ datetime datetime Time of check-in
|
||||
downloads list of objects List of ticket download options
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
@@ -124,6 +136,10 @@ answers list of objects Answers to user
|
||||
|
||||
The attribute ``tax_rule`` has been added.
|
||||
|
||||
.. versionchanged:: 1.11
|
||||
|
||||
The attribute ``checkins.list`` has been added.
|
||||
|
||||
|
||||
Order endpoints
|
||||
---------------
|
||||
@@ -166,6 +182,7 @@ Order endpoints
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"checkin_attention": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"is_business": True,
|
||||
@@ -198,6 +215,7 @@ Order endpoints
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
@@ -272,6 +290,7 @@ Order endpoints
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"checkin_attention": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"company": "Sample company",
|
||||
@@ -304,6 +323,7 @@ Order endpoints
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
@@ -342,7 +362,7 @@ Order endpoints
|
||||
|
||||
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
|
||||
be a ZIP file, PDF file or something else. The order details response contains a list of output options for this
|
||||
partictular order.
|
||||
particular order.
|
||||
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Note that in some cases the
|
||||
ticket file might not yet have been created. In that case, you will receive a status code :http:statuscode:`409` and
|
||||
@@ -622,6 +642,7 @@ Order position endpoints
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
@@ -701,6 +722,7 @@ Order position endpoints
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
Questions
|
||||
=========
|
||||
|
||||
@@ -23,15 +25,25 @@ type string The expected ty
|
||||
* ``C`` – choice from a list
|
||||
* ``M`` – multiple choice from a list
|
||||
* ``F`` – file upload
|
||||
* ``D`` – date
|
||||
* ``H`` – time
|
||||
* ``W`` – date and time
|
||||
required boolean If ``True``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
ask_during_checkin boolean If ``True``, this question will not be asked while
|
||||
buying the ticket, but will show up when redeeming
|
||||
the ticket instead.
|
||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||
available objects.
|
||||
├ id integer Internal ID of the option
|
||||
└ answer multi-lingual string The displayed value of this option
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
|
||||
been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -68,6 +80,7 @@ Endpoints
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -121,6 +134,7 @@ Endpoints
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
"ask_during_checkin": false,
|
||||
"position": 1,
|
||||
"options": [
|
||||
{
|
||||
|
||||
@@ -158,7 +158,7 @@ Endpoints
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/quotas/(id)/
|
||||
|
||||
Update a quota. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be resetted to default. With ``PATCH``, you only need to provide the fields that you
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field.
|
||||
|
||||
@@ -4,7 +4,8 @@ Tax rules
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Tax rules specify how tax should be calculated for specific products.
|
||||
Tax rules specify how tax should be calculated for specific products. Custom taxation rule sets are currently to
|
||||
available via the API.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
|
||||
11
doc/checkin_filter.py
Normal file
11
doc/checkin_filter.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from enchant.tokenize import get_tokenizer, Filter, unit_tokenize
|
||||
|
||||
class CheckinFilter(Filter):
|
||||
""" If a word looks like checkin_count, it refers to a so-called variable in
|
||||
the code, and is treated as being spelled right."""
|
||||
|
||||
def _split(self, word):
|
||||
if word[:8] == "checkin_":
|
||||
return unit_tokenize(word[8:])
|
||||
|
||||
return unit_tokenize(word)
|
||||
20
doc/conf.py
20
doc/conf.py
@@ -45,6 +45,7 @@ extensions = [
|
||||
'sphinx.ext.coverage',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinxcontrib.images',
|
||||
'sphinxcontrib.spelling',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@@ -290,3 +291,22 @@ texinfo_documents = [
|
||||
images_config = {
|
||||
'default_image_width': '250px'
|
||||
}
|
||||
|
||||
# -- Options for Spelling output ------------------------------------------
|
||||
|
||||
# String specifying the language, as understood by PyEnchant and enchant.
|
||||
# Defaults to en_US for US English.
|
||||
spelling_lang = 'en_US'
|
||||
|
||||
# String specifying a file containing a list of words known to be spelled
|
||||
# correctly but that do not appear in the language dictionary selected by
|
||||
# spelling_lang. The file should contain one word per line.
|
||||
spelling_word_list_filename='spelling_wordlist.txt'
|
||||
|
||||
# Boolean controlling whether suggestions for misspelled words are printed.
|
||||
# Defaults to False.
|
||||
spelling_show_suggestions=True
|
||||
|
||||
# List of filter classes to be added to the tokenizer that produces words to be checked.
|
||||
from checkin_filter import CheckinFilter
|
||||
spelling_filters=[CheckinFilter]
|
||||
|
||||
@@ -25,7 +25,7 @@ If you want to add a custom view to the control area of an event, just register
|
||||
views.admin_view, name='backend'),
|
||||
]
|
||||
|
||||
It is required that your URL paramaters are called ``organizer`` and ``event``. If you want to
|
||||
It is required that your URL parameters are called ``organizer`` and ``event``. If you want to
|
||||
install a view on organizer level, you can leave out the ``event``.
|
||||
|
||||
You can then implement the view as you would normally do. Our middleware will automatically
|
||||
|
||||
@@ -21,10 +21,10 @@ that we'll provide in this plugin::
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_data_exporter
|
||||
from pretix.base.signals import register_data_exporters
|
||||
|
||||
|
||||
@receiver(register_data_exporter, dispatch_uid="exporter_myexporter")
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_myexporter")
|
||||
def register_data_exporter(sender, **kwargs):
|
||||
from .exporter import MyExporter
|
||||
return MyExporter
|
||||
|
||||
@@ -11,7 +11,7 @@ Core
|
||||
----
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
|
||||
@@ -102,6 +102,8 @@ The provider class
|
||||
|
||||
.. automethod:: order_control_refund_perform
|
||||
|
||||
.. automethod:: is_implicit
|
||||
|
||||
|
||||
Additional views
|
||||
----------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Logging
|
||||
=======
|
||||
Logging and notifications
|
||||
=========================
|
||||
|
||||
As pretix is handling monetary transactions, we are very careful to make it possible to review all changes
|
||||
in the system that lead to the current state.
|
||||
@@ -19,7 +19,7 @@ To actually log an action, you can just call the ``log_action`` method on your o
|
||||
order.log_action('pretix.event.order.canceled', user=user, data={})
|
||||
|
||||
The positional ``action`` argument should represent the type of action and should be globally unique, we
|
||||
recomment do prefix it with your packagename, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
||||
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
||||
optional and may contain the user who performed the action. The optional ``data`` argument can contain
|
||||
additional information about this action.
|
||||
|
||||
@@ -81,6 +81,61 @@ implementation could look like::
|
||||
if logentry.action_type in plains:
|
||||
return plains[logentry.action_type]
|
||||
|
||||
Sending notifications
|
||||
---------------------
|
||||
|
||||
If you think that the logged information might be important or urgent enough to send out a notification to interested
|
||||
organizers. In this case, you should listen for the :py:attr:`pretix.base.signals.register_notification_types` signal
|
||||
to register a notification type::
|
||||
|
||||
@receiver(register_notification_types)
|
||||
def register_my_notification_types(sender, **kwargs):
|
||||
return [MyNotificationType(sender)]
|
||||
|
||||
Note that this event is different than other events send out by pretix: ``sender`` may be an event or ``None``. The
|
||||
latter case is required to let the user define global notification preferences for all events.
|
||||
|
||||
You also need to implement a custom class that specifies how notifications should be handled for your notification type.
|
||||
You should subclass the base ``NotificationType`` class and implement all its members:
|
||||
|
||||
.. autoclass:: pretix.base.notifications.NotificationType
|
||||
:members: action_type, verbose_name, required_permission, build_notification
|
||||
|
||||
A simple implementation could look like this::
|
||||
|
||||
class MyNotificationType(NotificationType):
|
||||
required_permission = "can_view_orders"
|
||||
action_type = "pretix.event.order.paid"
|
||||
verbose_name = _("Order has been paid")
|
||||
|
||||
def build_notification(self, logentry: LogEntry):
|
||||
order = logentry.content_object
|
||||
|
||||
order_url = build_absolute_uri(
|
||||
'control:event.order',
|
||||
kwargs={
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
'code': order.code
|
||||
}
|
||||
)
|
||||
|
||||
n = Notification(
|
||||
event=logentry.event,
|
||||
title=_('Order {code} has been marked as paid').format(code=order.code),
|
||||
url=order_url
|
||||
)
|
||||
n.add_attribute(_('Order code'), order.code)
|
||||
n.add_action(_('View order details'), order_url)
|
||||
return n
|
||||
|
||||
As you can see, the relevant code is in the ``build_notification`` method that is supposed to create a ``Notification``
|
||||
method that has a title, description, URL, attributes, and actions. The full definition of ``Notification`` is the
|
||||
following:
|
||||
|
||||
.. autoclass:: pretix.base.notifications.Notification
|
||||
:members: add_action, add_attribute
|
||||
|
||||
|
||||
Logging technical information
|
||||
-----------------------------
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. spelling:: answ contrib
|
||||
|
||||
Data model
|
||||
==========
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Forms
|
||||
-----
|
||||
|
||||
Hierarkey also provides a base class for forms that allow the modification of settings. pretix contains a
|
||||
subclass that also adds suport for internationalized fields:
|
||||
subclass that also adds support for internationalized fields:
|
||||
|
||||
.. autoclass:: pretix.base.forms.SettingsForm
|
||||
|
||||
@@ -65,4 +65,4 @@ Plugins can add custom hardcoded defaults in the following way::
|
||||
Make sure that you include this code in a module that is imported at app loading time.
|
||||
|
||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||
|
||||
@@ -67,7 +67,7 @@ available as ``plugins:sendmail:send``.
|
||||
Generating a URL for the frontend is a complicated task, because you need to know whether the event's
|
||||
organizer uses a custom URL or not and then generate the URL with a different domain and different
|
||||
arguments based on this information. pretix provides some helpers to make this easier. The first helper
|
||||
is a python method that emulates a behaviour similar to ``reverse``:
|
||||
is a python method that emulates a behavior similar to ``reverse``:
|
||||
|
||||
.. autofunction:: pretix.multidomain.urlreverse.eventreverse
|
||||
|
||||
@@ -82,5 +82,5 @@ Implementation details
|
||||
----------------------
|
||||
|
||||
There are some other caveats when using a design like this, e.g. you have to care about cookie domains
|
||||
and referer verification yourself. If you want to see how we built this, look into the ``pretix/multidomain/``
|
||||
and referrer verification yourself. If you want to see how we built this, look into the ``pretix/multidomain/``
|
||||
sub-tree.
|
||||
|
||||
@@ -86,7 +86,7 @@ and head to http://localhost:8000/
|
||||
|
||||
As we did not implement an overall front page yet, you need to go directly to
|
||||
http://localhost:8000/control/ for the admin view or, if you imported the test
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2018/
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
|
||||
|
||||
.. note:: If you want the development server to listen on a different interface or
|
||||
port (for example because you develop on `pretixdroid`_), you can check
|
||||
@@ -106,7 +106,7 @@ Execute the following commands to check for code style errors::
|
||||
isort -c -rc .
|
||||
python manage.py check
|
||||
|
||||
Execute the following command to run pretix' test suite (might take a coumple of minutes)::
|
||||
Execute the following command to run pretix' test suite (might take a couple of minutes)::
|
||||
|
||||
py.test
|
||||
|
||||
@@ -122,7 +122,7 @@ for example::
|
||||
flake8 . || exit 1
|
||||
isort -q -rc -c . || exit 1
|
||||
|
||||
This keeps you from accidentally creating commits violating the sdtyle guide.
|
||||
This keeps you from accidentally creating commits violating the style guide.
|
||||
|
||||
Working with mails
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -1,56 +1,10 @@
|
||||
.. spelling::
|
||||
Analytics
|
||||
|
||||
List of plugins
|
||||
===============
|
||||
|
||||
The following plugins are shipped with pretix and are supported in the same
|
||||
ways that pretix itself is:
|
||||
A detailed list of plugins that are available for pretix can be found on the
|
||||
`project website`_.
|
||||
|
||||
* Bank transfer
|
||||
* PayPal
|
||||
* Stripe
|
||||
* Check-in lists
|
||||
* pretixdroid
|
||||
* Report exporter
|
||||
* Send out emails
|
||||
* Statistics
|
||||
* PDF ticket output
|
||||
|
||||
The following plugins are not shipped with pretix but are maintained by the
|
||||
same team. We update them regularly to make them compatible with the latest
|
||||
pretix releases:
|
||||
|
||||
* `SEPA direct debit`_
|
||||
* `Wirecard payment`_
|
||||
* `Pages`_
|
||||
* `Passbook/Wallet ticket output`_
|
||||
* `Cartshare`_
|
||||
* `Fontpack Free fonts`_
|
||||
* `Mailing list subscription`_
|
||||
|
||||
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
|
||||
Please get in touch with the pretix team if you want to have them for your self-hosted
|
||||
pretix installation:
|
||||
|
||||
* Campaign tracking
|
||||
* Integration with Google Analytics and Facebook Pixel
|
||||
* Integration with Slack
|
||||
* Integration with MailChimp
|
||||
|
||||
The following plugins are from independent third-party authors, so we can make
|
||||
no statements about their functionality, security, stability or compatibility:
|
||||
|
||||
* `esPass ticket output`_
|
||||
* `IcePay integration`_
|
||||
* `Average price chart`_
|
||||
* `Pay in cash upon arrival`_
|
||||
|
||||
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
|
||||
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
|
||||
.. _Cartshare: https://github.com/pretix/pretix-cartshare
|
||||
.. _Pages: https://github.com/pretix/pretix-pages
|
||||
.. _esPass ticket output: https://github.com/esPass/pretix-espass
|
||||
.. _IcePay integration: https://github.com/chotee/pretix-icepay
|
||||
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
|
||||
.. _Wirecard payment: https://github.com/pretix/pretix-wirecard
|
||||
.. _Mailing list subscription: https://github.com/pretix/pretix-newsletter-ml
|
||||
.. _Average price chart: https://github.com/rixx/pretix-avgchart
|
||||
.. _Pay in cash upon arrival: https://github.com/pc-coholic/pretix-cashpayment
|
||||
.. _project website: https://pretix.eu/about/en/plugins
|
||||
|
||||
@@ -9,6 +9,17 @@ uses to communicate with the pretix server.
|
||||
general-purpose :ref:`rest-api` that not yet provides all features that this API provides, but will do
|
||||
so in the future.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
Support for check-in-time questions has been added. The new API features are fully backwards-compatible and
|
||||
negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version
|
||||
has not been increased and is still set to 3.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
Support for checking in unpaid tickets has been added.
|
||||
|
||||
|
||||
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
|
||||
|
||||
Redeems a ticket, i.e. checks the user in.
|
||||
@@ -22,18 +33,33 @@ uses to communicate with the pretix server.
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3
|
||||
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3&questions_supported=true
|
||||
|
||||
You can optionally include the additional parameter ``datetime`` in the body containing an ISO8601-encoded
|
||||
datetime of the entry attempt. If you don't, the current date and time will be used.
|
||||
You **must** set the parameter secret.
|
||||
|
||||
You can optionally include the additional parameter ``force`` to indicate that the request should be logged
|
||||
You **must** set the parameter ``questions_supported`` to ``true`` **if** you support asking questions
|
||||
back to the app operator. You **must not** set it if you do not support this feature. In that case, questions
|
||||
will just be ignored.
|
||||
|
||||
You **may** set the additional parameter ``datetime`` in the body containing an ISO8601-encoded
|
||||
datetime of the entry attempt. If you don"t, the current date and time will be used.
|
||||
|
||||
You **may** set the additional parameter ``force`` to indicate that the request should be logged
|
||||
regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline.
|
||||
Questions will also always be ignored in this case (i.e. supplied answers will be saved, but no error will be
|
||||
thrown if they are missing or invalid).
|
||||
|
||||
You can optionally include the additional parameter ``nonce`` with a globally unique random value to identify this
|
||||
You **may** set the additional parameter ``nonce`` with a globally unique random value to identify this
|
||||
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
|
||||
failure.
|
||||
|
||||
You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even
|
||||
if the order is in pending state.
|
||||
|
||||
If questions are supported and required, you will receive a dictionary ``questions`` containing details on the
|
||||
particular questions to ask. To answer them, just re-send your redemption request with additional parameters of
|
||||
the form ``answer_<question>=<answer>``, e.g. ``answer_12=24``.
|
||||
|
||||
**Example successful response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -43,10 +69,68 @@ uses to communicate with the pretix server.
|
||||
|
||||
{
|
||||
"status": "ok"
|
||||
"version": 2
|
||||
"version": 3,
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
|
||||
**Example error response**:
|
||||
**Example response with required questions**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "incomplete"
|
||||
"version": 3
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "C",
|
||||
"question": "Choose a shirt size",
|
||||
"required": true,
|
||||
"position": 2,
|
||||
"items": [1],
|
||||
"options": [
|
||||
{
|
||||
"id": 24,
|
||||
"answer": "M"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"answer": "L"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
**Example error response with data**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
@@ -56,13 +140,40 @@ uses to communicate with the pretix server.
|
||||
{
|
||||
"status": "error",
|
||||
"reason": "already_redeemed",
|
||||
"version": 2
|
||||
"version": 3,
|
||||
"data": {
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
"order": "ABCDE",
|
||||
"item": "Standard ticket",
|
||||
"item_id": 1,
|
||||
"variation": null,
|
||||
"variation_id": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
|
||||
**Example error response without data**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "error",
|
||||
"reason": "unkown_ticket",
|
||||
"version": 3
|
||||
}
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``unpaid`` - Ticket is not paid for or has been refunded
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
* ``unknown_ticket`` - Secret does not match a ticket in the database
|
||||
|
||||
:query key: Secret API key
|
||||
@@ -100,11 +211,12 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"version": 2
|
||||
"version": 3
|
||||
}
|
||||
|
||||
:query query: Search query
|
||||
@@ -133,6 +245,7 @@ uses to communicate with the pretix server.
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"version": 3,
|
||||
"results": [
|
||||
{
|
||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
||||
@@ -142,11 +255,31 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"version": 2
|
||||
"questions": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "C",
|
||||
"question": "Choose a shirt size",
|
||||
"required": true,
|
||||
"position": 2,
|
||||
"items": [1],
|
||||
"options": [
|
||||
{
|
||||
"id": 24,
|
||||
"answer": "M"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"answer": "L"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query key: Secret API key
|
||||
@@ -157,7 +290,7 @@ uses to communicate with the pretix server.
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
|
||||
|
||||
Returns status information, such as the total number of tickets and the
|
||||
number of performed checkins.
|
||||
number of performed check-ins.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -177,7 +310,7 @@ uses to communicate with the pretix server.
|
||||
{
|
||||
"checkins": 17,
|
||||
"total": 42,
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"event": {
|
||||
"name": "Demo Converence",
|
||||
"slug": "democon",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-r ../src/requirements.txt
|
||||
sphinx
|
||||
sphinx==1.6.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-spelling
|
||||
pyenchant
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
119
doc/spelling_wordlist.txt
Normal file
119
doc/spelling_wordlist.txt
Normal file
@@ -0,0 +1,119 @@
|
||||
addon
|
||||
addons
|
||||
api
|
||||
auth
|
||||
autobuild
|
||||
backend
|
||||
backends
|
||||
banktransfer
|
||||
boolean
|
||||
booleans
|
||||
cancelled
|
||||
casted
|
||||
checkbox
|
||||
checksum
|
||||
config
|
||||
contenttypes
|
||||
contextmanager
|
||||
cron
|
||||
cronjob
|
||||
debian
|
||||
deduplication
|
||||
discoverable
|
||||
django
|
||||
dockerfile
|
||||
durations
|
||||
eu
|
||||
filename
|
||||
filesystem
|
||||
fontawesome
|
||||
frontend
|
||||
frontpage
|
||||
gettext
|
||||
gunicorn
|
||||
hardcoded
|
||||
hostname
|
||||
invalidations
|
||||
iterable
|
||||
libsass
|
||||
linters
|
||||
memcached
|
||||
metadata
|
||||
middleware
|
||||
mixin
|
||||
mixins
|
||||
multi
|
||||
multidomain
|
||||
namespace
|
||||
namespaced
|
||||
namespaces
|
||||
namespacing
|
||||
natively
|
||||
nginx
|
||||
NotificationType
|
||||
ons
|
||||
optimizations
|
||||
param
|
||||
percental
|
||||
positionid
|
||||
pre
|
||||
prepend
|
||||
prepended
|
||||
prepending
|
||||
preprocessor
|
||||
presale
|
||||
pretix
|
||||
pretixdroid
|
||||
pretixpresale
|
||||
prometheus
|
||||
proxied
|
||||
proxying
|
||||
queryset
|
||||
redemptions
|
||||
redis
|
||||
refactored
|
||||
regex
|
||||
renderer
|
||||
renderers
|
||||
reportlab
|
||||
screenshot
|
||||
serializers
|
||||
serializers
|
||||
sexualized
|
||||
startup
|
||||
stdout
|
||||
stylesheet
|
||||
subdirectories
|
||||
subdirectory
|
||||
subdomain
|
||||
subdomains
|
||||
subevent
|
||||
subevents
|
||||
submodule
|
||||
subpath
|
||||
systemd
|
||||
testutils
|
||||
timestamp
|
||||
un
|
||||
unconfigured
|
||||
unix
|
||||
unprefixed
|
||||
untrusted
|
||||
username
|
||||
url
|
||||
versa
|
||||
viewset
|
||||
viewsets
|
||||
webhook
|
||||
webhooks
|
||||
webserver
|
||||
webservice
|
||||
workflow
|
||||
zipcode
|
||||
Datetime
|
||||
Embeddable
|
||||
Hierarkey
|
||||
OAuth
|
||||
SSL
|
||||
Uptime
|
||||
Yay
|
||||
@@ -30,13 +30,13 @@ available products, quotas, prices and some meta information, but most settings
|
||||
We recommend to use this feature only if you really know that you need it and if you really run a lot of events, not if
|
||||
you run e.g. a yearly conference. You can read more on this feature :ref:`here <subevents>`.
|
||||
|
||||
Once you set these values, you can procede to the next step:
|
||||
Once you set these values, you can proceed to the next step:
|
||||
|
||||
.. thumbnail:: ../../screens/event/create_step2.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
In this step, you will be asked more detailled questions about your event. In particular, you can fill in the
|
||||
In this step, you will be asked more detailed questions about your event. In particular, you can fill in the
|
||||
following fields:
|
||||
|
||||
Name
|
||||
|
||||
@@ -8,13 +8,13 @@ The settings at "Settings" → "Display" allow you to customize the appearance o
|
||||
:class: screenshot
|
||||
|
||||
The upper part of the page contains settings that you always need to set specifically for your event. Those are
|
||||
currently::
|
||||
currently:
|
||||
|
||||
Logo image
|
||||
This logo will be shown as a banner above your shop. If you set it, the event name and date will no longer be
|
||||
displayed by the shop, so we suggest to include them in the image yourself. The maximal height of the image is
|
||||
120 pixels and if you want to use the full width, make your image 1140 pixels wide. If the user's screen is
|
||||
smaller, the logo will be scaled down automatically, so it should still be legigible at smaller sizes.
|
||||
smaller, the logo will be scaled down automatically, so it should still be legible at smaller sizes.
|
||||
|
||||
Frontpage text
|
||||
This text will be shown on the front page of your ticket shop, above the list of products. You can use it to explain
|
||||
@@ -39,4 +39,4 @@ Font
|
||||
Choose one of multiple fonts to use for your web shop.
|
||||
|
||||
.. note:: Both the color and font settings can take a few seconds up to a few minutes before they become active on your
|
||||
shop.
|
||||
shop.
|
||||
|
||||
129
doc/user/events/email.rst
Normal file
129
doc/user/events/email.rst
Normal file
@@ -0,0 +1,129 @@
|
||||
E-mail settings
|
||||
===============
|
||||
|
||||
The settings at "Settings" → "E-mail" allow you to customize the emails that pretix sends to the participants of your
|
||||
event.
|
||||
|
||||
.. thumbnail:: ../../screens/event/settings_email.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
The page is separated into three parts: "E-mail settings", "E-mail content" and "SMTP settings". We will explain all
|
||||
of them in detail on this page.
|
||||
|
||||
E-mail settings
|
||||
---------------
|
||||
|
||||
The upper part of the page contains settings that are relevant for the generation of all e-mails alike. Those are
|
||||
currently:
|
||||
|
||||
Subject prefix
|
||||
This text will be prepended to the subject of all e-mails that are related to your event. For example, if you
|
||||
set this to "dc2018" all subjects will be formatted like "[dc2018] Your payment was successful".
|
||||
|
||||
Sender address
|
||||
All e-mails will be sent with this address in the "From" field. If you use an email address at a custom domain,
|
||||
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
|
||||
due to the `Sender Policy Framework`_ and similar mechanisms.
|
||||
|
||||
Signature
|
||||
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
|
||||
details or any legal information that needs to be included with the e-mails.
|
||||
|
||||
E-mail content
|
||||
--------------
|
||||
|
||||
The middle part of the page allows you to customize the exact texts of all e-mails sent by the system automatically.
|
||||
You can click on the different boxes to expand them and see the texts.
|
||||
|
||||
Within the texts, you can use placeholders that will later by replaced by values depending on the event or order. Below
|
||||
every text box is a list of supported placeholders, but currently the following are defined (not every placeholder
|
||||
is valid in every text):
|
||||
|
||||
============================== ===============================================================================
|
||||
Placeholder Description
|
||||
============================== ===============================================================================
|
||||
event The event name
|
||||
total The order's total value
|
||||
currency The currency used for the event (three-letter code)
|
||||
payment_info Information text specific to the payment method (e.g. banking details)
|
||||
url An URL pointing to the download/status page of the order
|
||||
invoice_name The name field of the invoice address
|
||||
invoice_company The company field of the invoice address
|
||||
expire_date The order's expiration date
|
||||
date The same as ``expire_date``, but in a different e-mail (for backwards
|
||||
compatibility)
|
||||
orders A list of orders including links to their status pages, specific to the "resend
|
||||
link (requested by user)" e-mail
|
||||
code In case of the waiting list, the voucher code to redeem
|
||||
hours In case of the waiting list, the number of hours the voucher code is valid
|
||||
============================== ===============================================================================
|
||||
|
||||
The different e-mails are explained in the following:
|
||||
|
||||
Placed Order
|
||||
This e-mail is sent out to every order directly after the order has been received, except if the order total
|
||||
is zero (see below). It should specify that/how the order is to be paid.
|
||||
|
||||
Paid Order
|
||||
This e-mail is sent out as soon as the payment for an order has been received and should give the customer
|
||||
more information on how to proceed, e.g. by downloading their ticket.
|
||||
|
||||
Free Order
|
||||
This e-mail is sent out instead of "Placed Order" and "Paid Order" if the order total is zero. It therefore should
|
||||
tell the same information, except asking the customer for completing their payment.
|
||||
|
||||
Resend link
|
||||
Sent by admin
|
||||
This e-mail will be sent out if you click the "Resend link" next to the e-mail address field on the order detail
|
||||
page. It should include the link to the order and can be sent to users e.g. if they lost their original e-mails.
|
||||
|
||||
Requested by user
|
||||
Customers can also request a link to all orders they created using their e-mail address themselves by filling
|
||||
out a form on the website. In this case, they will receive an e-mail containing a list of all orders they created
|
||||
with the respective links.
|
||||
|
||||
Order changed
|
||||
This e-mail is sent out if you change the content of the order and choose to notify the user about it.
|
||||
|
||||
Payment reminder
|
||||
This e-mail is sent out a certain number of days before the order's expiry date. You can specify the number of days
|
||||
before the expiry date that this should happen and the e-mail will only ever be sent if you do specify such a
|
||||
number. The text should ask the customer to complete the payment, tell the options on how to do so and the
|
||||
consequences if no payment is received (ticket gone, depending on your other settings). You should also include
|
||||
a way to contact you in case of questions.
|
||||
|
||||
Waiting list notification
|
||||
If you enable the waiting list feature, this is the mail that will be sent out if a ticket is assigned to a person on
|
||||
the waiting list. It should include the voucher that needs to be redeemed to get the free spot and tell how long
|
||||
that voucher is valid and where to redeem it.
|
||||
|
||||
Order canceled
|
||||
This e-mail is sent to a customer if their order has been canceled.
|
||||
|
||||
|
||||
Order custom mail
|
||||
You can use pretix' admin interface to directly send an e-mail with a custom text to the customer of a specific
|
||||
order. In this case, this will be the default text and might save you time by not having to re-type all of it every
|
||||
time.
|
||||
|
||||
Reminder to download tickets
|
||||
If you want, you can configure an email that will be send out a number of days before your event to remind
|
||||
attendees to download their tickets. The e-mail should include a link to the ticket download. This e-mail will only
|
||||
ever be sent if you specify a number of days.
|
||||
|
||||
SMTP settings
|
||||
-------------
|
||||
|
||||
If you want to send your e-mails via your own e-mail address, we strongly recommend to use SMTP for this purpose.
|
||||
SMTP is a protocol that is used by e-mail clients to communicate with e-mail servers. Using SMTP, pretix can talk to
|
||||
your e-mail service provider the same way that e.g. the e-mail app on your phone can.
|
||||
|
||||
Your e-mail provider will most likely have a document that tells you the settings for the various fields to fill in
|
||||
here (hostname, port, username, password, encryption).
|
||||
|
||||
With the checkbox "Use custom SMTP server" you can turn using your SMTP server on or off completely. With the
|
||||
button "Save and test custom SMTP connection", you can test if the connection and authentication to your SMTP server
|
||||
succeeds, even before turning that checkbox on.
|
||||
|
||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||
96
doc/user/events/invoicing.rst
Normal file
96
doc/user/events/invoicing.rst
Normal file
@@ -0,0 +1,96 @@
|
||||
Invoice settings
|
||||
================
|
||||
|
||||
.. spelling:: Inv
|
||||
|
||||
The settings at "Settings" → "Invoice" allow you to specify if and how pretix should generate invoices for your orders.
|
||||
|
||||
.. thumbnail:: ../../screens/event/settings_invoice.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
In particular, you can configure the following things:
|
||||
|
||||
Ask for invoice address
|
||||
If this checkbox is enabled, customers will be able to enter an invoice address during checkout. If you only enable
|
||||
this box, the invoice address will be optional to fill in.
|
||||
|
||||
Require invoice address
|
||||
If this checkbox is enabled, entering an invoice address will be obligatory for all customers and it will not be
|
||||
able to create an order without entering an address.
|
||||
|
||||
Require customer name
|
||||
If this checkbox is enabled, the street, city, and country fields of the invoice address will still be optional but
|
||||
the name field will be obligatory.
|
||||
|
||||
Generate invoices
|
||||
This field controls whether pretix should generate an invoice for an order. You have the following options:
|
||||
|
||||
No
|
||||
pretix will never generate an invoice. If you want to issue invoices, you need to do it yourself based on the
|
||||
collected address data.
|
||||
|
||||
Manually in admin panel
|
||||
pretix will not create invoices automatically, but the order detail view will show a button that allows you to
|
||||
manually generate one for specific orders.
|
||||
|
||||
Automatically on user request
|
||||
pretix will not create invoices on its own, but both the panel as well as the customer view of the order will
|
||||
show a button that instantly generates an invoice for the specified order.
|
||||
|
||||
Automatically for all created orders
|
||||
pretix will automatically create an invoice every time an order is placed.
|
||||
|
||||
Automatically on payment
|
||||
pretix will automatically create an invoice for an order, as soon as the payment for the order is received.
|
||||
|
||||
pretix will never generate invoices for free orders, even though it might ask for the invoice address.
|
||||
|
||||
Attach invoices to emails
|
||||
If enabled, invoices will be attached to order confirmation e-mails if the "Generate invoices" setting is set to
|
||||
"Automatically for all created orders" or to the payment confirmation e-mails if it is set to "Automatically on
|
||||
payment".
|
||||
|
||||
Ask for VAT ID
|
||||
If enabled, the invoice address form will not only ask for a postal address, but also for a VAT ID. The VAT ID will
|
||||
always be an optional field.
|
||||
|
||||
Generate invoices with consecutive numbers
|
||||
If enabled, invoices will be created with numerical invoice numbers in the order of their creation, i.e.
|
||||
PREFIX-00001, PREFIX-00002, and so on. If disabled, invoice numbers will instead be generated from the order code,
|
||||
i.e. PREFIX-YHASD-1. When in doubt, keep this option enabled since it might be legally required in your country,
|
||||
but disabling it has the advantage that your customers can not estimate the number of tickets sold by looking at
|
||||
the invoice numbers.
|
||||
|
||||
Invoice number prefix
|
||||
This is the prefix that will be prepended to all your invoice numbers. For example, if you set this to "Inv", your
|
||||
invoices will be numbered Inv00001, Inv00002, etc. If you leave this field empty, your event slug will be used,
|
||||
followed by a dash, e.g. DEMOCON-00001.
|
||||
|
||||
Within one organizer account, events with the same number prefix will share their number range. For example, if you
|
||||
set this to "Inv" for all of your events, there will be only one invoice numbered Inv00007 across all your events
|
||||
and the numbers will have gaps within one event.
|
||||
|
||||
Show free products on invoices
|
||||
If enabled, products that do not cost anything will still show up on invoices. Note that the order needs to contain
|
||||
at least one non-free product in order to generate an invoice.
|
||||
|
||||
Show attendee names on invoices
|
||||
If enabled, the attendee name will be printed on the invoice for admission tickets.
|
||||
|
||||
Your address
|
||||
This should be set to the address of the entity issuing the invoice (read: you) and will be printed inside
|
||||
the header of the invoice.
|
||||
|
||||
Introductory text
|
||||
A free custom text that will be printed above the list of products on the invoice.
|
||||
|
||||
Additional text
|
||||
A free custom text that will be printed below the list of products and the invoice total.
|
||||
|
||||
Footer
|
||||
A text that will be printed in the foot line of the invoice. This could contain your contact details or legal
|
||||
information on the issuing entity, e.g. registration numbers, your VAT ID, etc.
|
||||
|
||||
Logo image
|
||||
A square image that will be printed in the invoice header, currently with a width of 2.5cm.
|
||||
@@ -8,4 +8,7 @@ Configuring an event
|
||||
../payments/index
|
||||
plugins
|
||||
display
|
||||
tickets
|
||||
email
|
||||
taxes
|
||||
invoicing
|
||||
|
||||
@@ -103,7 +103,7 @@ End of presale
|
||||
|
||||
Quotas
|
||||
As for all events, no tickets will be available unless there is a quota created for them that specifies the number
|
||||
of tickets available. You can create multiple quotas that are assinged to this date directly from this interface.
|
||||
of tickets available. You can create multiple quotas that are assigned to this date directly from this interface.
|
||||
|
||||
Item prices
|
||||
This is a table of all products configured for your shop. If you want, you can enter a new price for each one of them
|
||||
|
||||
@@ -18,7 +18,7 @@ your event, go to the respective section in your event's settings:
|
||||
:class: screenshot
|
||||
|
||||
On this page, you can create, edit and delete your tax rules. Clicking on the name of a tax rule will take you to its
|
||||
detailled settings:
|
||||
detailed settings:
|
||||
|
||||
.. thumbnail:: ../../screens/event/tax_detail.png
|
||||
:align: center
|
||||
@@ -100,6 +100,16 @@ taxes" at the end of the page.
|
||||
errors of usually up to one cent from the intended price. This is unavoidable due to the
|
||||
flexible nature in which prices are being calculated.
|
||||
|
||||
Custom tax rules
|
||||
----------------
|
||||
|
||||
If you have very special requirements for the conditions in which VAT will or will not be charged, you can use the
|
||||
"Custom tax rules" section instead of the options listed above. Here, you can create a set of rules consisting of
|
||||
conditions (i.e. a country or a type of customer) and actions (i.e. do or do not charge VAT).
|
||||
|
||||
The rules will then be checked from top to bottom and the first matching rule will be used to decide if VAT will be
|
||||
charged to the user.
|
||||
|
||||
Taxation of payment fees
|
||||
------------------------
|
||||
|
||||
|
||||
30
doc/user/events/tickets.rst
Normal file
30
doc/user/events/tickets.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
Ticket settings
|
||||
===============
|
||||
|
||||
At "Settings" → "Tickets", you can configure the ticket download options that will be presented to your customers:
|
||||
|
||||
.. thumbnail:: ../../screens/event/settings_tickets.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
The top of this page shows a short list of options relevant for all download formats:
|
||||
|
||||
Use feature
|
||||
This can be used to completely enable or disable ticket downloads all over your ticket shop.
|
||||
|
||||
Download date
|
||||
If you set a date here, no ticket download will be offered before this date. If no date is set, tickets can be
|
||||
downloaded immediately after the payment for an order has been received.
|
||||
|
||||
Offer to download tickets separately for add-on products
|
||||
By default, tickets can not be downloaded for order positions which are only an add-on to other order positions. If
|
||||
you enable this, this behavior will be changed and add-on products will get their own tickets as well. If disabled,
|
||||
you can still print a list of chosen add-ons e.g. on the PDF tickets.
|
||||
|
||||
Generate tickets for non-admission products
|
||||
By default, tickets will only be generated for products that are marked as admission products. Enable this option to
|
||||
generate tickets for all products instead.
|
||||
|
||||
Below these settings, the detail settings for the various ticket file formats are offered. They differ from format to
|
||||
format and only share the common "Enable" setting that can be used to turn them on. By default, pretix ships with
|
||||
a PDF output plugin that you can configure through a visual design editor.
|
||||
@@ -67,7 +67,7 @@ SSL
|
||||
---
|
||||
|
||||
Since buying a ticket normally involves entering sensitive data, we strongly suggest that you use SSL/HTTPS for the page
|
||||
that includes the widget. Initiatives like `Let's Encrypt`_ allow you to obtain a SSL certificat free of charge.
|
||||
that includes the widget. Initiatives like `Let's Encrypt`_ allow you to obtain a SSL certificate free of charge.
|
||||
|
||||
All data transferred to pretix will be made over SSL, even if using the widget on a non-SSL site. However, without
|
||||
using SSL for your site, a man-in-the-middle attacker could potentially alter the widget in dangerous ways. Moreover,
|
||||
@@ -75,7 +75,7 @@ using SSL is becoming standard practice and your customers might want expect see
|
||||
granted to SSL-enabled web pages.
|
||||
|
||||
By default, the checkout process will open in a new tab in your customer's browsers if you don't use SSL for your
|
||||
website. If you confident to have a good reason for not using SSL, you can override this behaviour with the
|
||||
website. If you confident to have a good reason for not using SSL, you can override this behavior with the
|
||||
``skip-ssl-check`` attribute::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
|
||||
@@ -101,4 +101,43 @@ voucher's settings.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
pretix Button
|
||||
-------------
|
||||
|
||||
Instead of a product list, you can also display just a single button. When pressed, the button will add a number of
|
||||
products associated with the button to the cart and will immediately proceed to checkout if the operation succeeded.
|
||||
You can try out this behavior here:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">Buy ticket!</pretix-button>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<br><br>
|
||||
|
||||
You can embed the pretix Button just like the pretix Widget. Just like above, first embed the CSS and JavaScript
|
||||
resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button`` tag::
|
||||
|
||||
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">
|
||||
Buy ticket!
|
||||
</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.
|
||||
|
||||
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
||||
|
||||
You can style the button using the ``pretix-button`` CSS class.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The pretix Button has been added in version 1.13.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
|
||||
@@ -9,7 +9,7 @@ There are multiple ways to do this.
|
||||
First, you could just create some orders in your real shop and cancel/refund them later. If you don't want to process
|
||||
real payments for the tests, you can either use a "manual" payment method like bank transfer and just mark the orders
|
||||
as paid with the button in the backend, or if you want to use e.g. Stripe, you can configure pretix to use your keys
|
||||
for the Stripe test sytem and use their test credit cars. Read our :ref:`Stripe documentation <stripe>` for more
|
||||
for the Stripe test system and use their test credit cars. Read our :ref:`Stripe documentation <stripe>` for more
|
||||
information.
|
||||
|
||||
Second, you could create a separate event, just for testing. In the last step of the :ref:`event creation process <event_create>`,
|
||||
@@ -24,9 +24,11 @@ received any real orders (i.e. taken the shop public). We won't charge any fees
|
||||
How do I delete an event?
|
||||
-------------------------
|
||||
|
||||
It is currently not possible to delete events, you can just disable the shop by clicking the first square on your event
|
||||
dashboard. Events can't be deleted as they most likely contain information on financial transactions which legally
|
||||
needs to be kept on record for multiple years in most countries.
|
||||
You can find the event deletion button at the bottom of the event settings page. Note however, that it is not possible
|
||||
to delete an event once any order or invoice has been created, as those likely contain information on financial
|
||||
transactions which legally may not be tampered with and needs to be kept on record for multiple years in most
|
||||
countries. In this case, you can just disable the shop by clicking the first square on your event
|
||||
dashboard.
|
||||
|
||||
If you are using the hosted service at pretix.eu and want to get rid of an event that you only used for testing, contact
|
||||
us at support@pretix.eu and we can remove it for you.
|
||||
@@ -48,4 +50,4 @@ If you created a product and it doesn't show up, please follow the following ste
|
||||
variation is contained in a quota. If your event is an event series, make sure that the product is contained in a
|
||||
quota that is assigned to the series date that you access the shop for.
|
||||
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
|
||||
your event.
|
||||
your event.
|
||||
|
||||
@@ -25,7 +25,7 @@ To set a text in italics, you can put it in asterisks or underscores. For exampl
|
||||
|
||||
will become:
|
||||
|
||||
Please *really* pay your _ticket_.
|
||||
Please *really* pay your *ticket*.
|
||||
|
||||
If you set double asterisks or underscores, the text will be printed in bold. For example,
|
||||
|
||||
@@ -145,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
|
||||
security reasons you can only use the following HTML elements::
|
||||
|
||||
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, span, strong,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
|
||||
table, tbody, td, thead, tr, ul
|
||||
|
||||
Additionally, only the following attributes are allowed on them::
|
||||
@@ -163,4 +163,4 @@ All other elements and attributes will be stripped during parsing.
|
||||
|
||||
|
||||
.. _Markdown: https://en.wikipedia.org/wiki/Markdown
|
||||
.. _Wikipedia: https://en.wikipedia.org
|
||||
.. _Wikipedia: https://en.wikipedia.org
|
||||
|
||||
@@ -47,11 +47,11 @@ Permissions separate into two areas:
|
||||
* Can change product settings – This permission allows to create and modify products and objects that are closely
|
||||
related to products, such as product categories, quotas, and questions.
|
||||
|
||||
* Can view orders – This permission allows viewing the list of orders and allindividual order details, but not
|
||||
* Can view orders – This permission allows viewing the list of orders and all individual order details, but not
|
||||
changing anything about it. This also includes the various exports offered.
|
||||
|
||||
* Can change orders – This permission allows all actions that involve changing an order, such as changing the products
|
||||
in an order, marking an order as paid or refunden, importing banking data, etc. This only works properly if the
|
||||
in an order, marking an order as paid or refunded, importing banking data, etc. This only works properly if the
|
||||
same users also have the "Can view orders" permission.
|
||||
|
||||
* Can view vouchers – This permission allows viewing the list of vouchers including the voucher codes themselves and
|
||||
|
||||
@@ -60,4 +60,4 @@ same 5 %, such that for a ticket with a list price of 100 € you will get your
|
||||
===================================================== =============
|
||||
|
||||
Due to the various rounding steps performed by pretix and by the payment provider, the end total on
|
||||
your bank account might stil vary by one cent.
|
||||
your bank account might still vary by one cent.
|
||||
|
||||
@@ -2,10 +2,10 @@ Payment method overview
|
||||
=======================
|
||||
|
||||
pretix allows you to accept payments using a variety of payment methods to fit the needs of very different events.
|
||||
This page gives you a short overview over them and links to more detailled descriptions in some cases.
|
||||
This page gives you a short overview over them and links to more detailed descriptions in some cases.
|
||||
|
||||
Payment methods are built as pretix plugins. For this reason, you might first need to enable a certain plugin at
|
||||
"Settings" → "Plugins" in your event settings. Then, you can configure them in detail at "Settings" -> "Payment".
|
||||
"Settings" → "Plugins" in your event settings. Then, you can configure them in detail at "Settings" → "Payment".
|
||||
|
||||
If you host pretix on your own server, you might need to install a plugin first for some of the payment methods listed
|
||||
on this page as well as for additional ones.
|
||||
@@ -13,5 +13,4 @@ on this page as well as for additional ones.
|
||||
To get an overview of the officially supported payment methods and their pros and cons, head to the `pretix website`_.
|
||||
On these pages, you get more information on how to configure :ref:`stripe`, :ref:`paypal`, and :ref:`banktransfer`.
|
||||
|
||||
|
||||
.. _pretix website: https://pretix.eu/about/en/payments
|
||||
.. _pretix website: https://pretix.eu/about/en/features/payment
|
||||
|
||||
@@ -12,6 +12,12 @@ If you look into pretix' settings, you are required to fill in two keys:
|
||||
|
||||
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
|
||||
need to go to `developer.paypal.com`_ to link the account to your pretix event.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unfortunately, PayPal tries to confuse you by having multiple APIs with different keys. You really need to
|
||||
go to https://developer.paypal.com for the API we use, not to your normal account settings!
|
||||
|
||||
Click on "Log In" in the top-right corner and log in with your PayPal account.
|
||||
|
||||
.. image:: img/paypal2.png
|
||||
@@ -46,8 +52,8 @@ webhooks. To create one, scroll a bit down and click "Add Webhook".
|
||||
.. image:: img/paypal7.png
|
||||
:class: screenshot
|
||||
|
||||
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
|
||||
screenshot but contain your event name. Tick the box "All events" and save.
|
||||
Then, enter the webhook URL that you find on the pretix settings page. If you use pretix Hosted, this is always ``https://pretix.eu/_paypal/webhook/``.
|
||||
Tick the box "All events" and save.
|
||||
|
||||
.. image:: img/paypal8.png
|
||||
:class: screenshot
|
||||
|
||||
@@ -11,7 +11,7 @@ of the page shows a number of general settings that affect all payment methods:
|
||||
In particular, these are:
|
||||
|
||||
Payment term in days
|
||||
If a order has been created, it is supposed to be paid within this number of days. Of course, some payment mehtods
|
||||
If a order has been created, it is supposed to be paid within this number of days. Of course, some payment methods
|
||||
(like credit card) succeed immediately in most cases, but others don't (like bank transfer) and even credit card
|
||||
payments might fail and you might want to give the customer a chance to try another credit card before losing their
|
||||
ticket. Therefore, we recommend setting a few days here. If you are accepting bank transfers, we wouldn't recommend
|
||||
@@ -19,7 +19,7 @@ Payment term in days
|
||||
|
||||
Last date of payments
|
||||
There is probably no use for payments received after your event, so you can set a date that the payment deadline of
|
||||
a new order will never exceed. This has precendence over the number of days configured above, so if I create an order
|
||||
a new order will never exceed. This has precedence over the number of days configured above, so if I create an order
|
||||
two days before the configured last date of payments, my payment term will only be two days, not ten. If you have
|
||||
payment methods that always require some time (like bank transfer), you will later be able to selectively disable them
|
||||
once the event comes closer.
|
||||
|
||||
34
res/logo.svg
34
res/logo.svg
@@ -9,15 +9,16 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="294.15625"
|
||||
height="149.59375"
|
||||
width="600"
|
||||
height="400"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:version="0.92.1 r"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/raphael/proj/pretix/pretix/logo_draft.png"
|
||||
inkscape:export-xdpi="88.529999"
|
||||
inkscape:export-ydpi="88.529999">
|
||||
inkscape:export-filename="/tmp/LOGO.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
viewBox="0 0 562.50001 375.00002">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
@@ -28,14 +29,14 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.9899495"
|
||||
inkscape:cx="134.70089"
|
||||
inkscape:cy="277.43904"
|
||||
inkscape:cx="133.36756"
|
||||
inkscape:cy="276.10571"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
inkscape:window-x="3200"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="0"
|
||||
fit-margin-top="20"
|
||||
@@ -58,11 +59,14 @@
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-257.78125,-548.75)">
|
||||
transform="translate(-259.03125,-322.09374)">
|
||||
<path
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#3b1c4a;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;enable-background:accumulate"
|
||||
d="M 20 20 L 20 54.09375 C 31.43679 54.09375 40.71875 63.37571 40.71875 74.8125 C 40.71875 86.24928 31.43679 95.5 20 95.5 L 20 129.59375 L 166.6875 129.59375 L 166.6875 120.09375 L 169.6875 120.09375 L 169.6875 129.59375 L 274.15625 129.59375 L 274.15625 95.5 C 274.14575 95.50002 274.1354 95.5 274.125 95.5 C 262.68822 95.5 253.40625 86.24928 253.40625 74.8125 C 253.40625 63.37571 262.68822 54.09375 274.125 54.09375 C 274.1355 54.09375 274.14585 54.09373 274.15625 54.09375 L 274.15625 20 L 169.6875 20 L 169.6875 29.09375 L 166.6875 29.09375 L 166.6875 20 L 20 20 z M 166.6875 36.09375 L 169.6875 36.09375 L 169.6875 50.09375 L 166.6875 50.09375 L 166.6875 36.09375 z M 208.12891 48.927734 C 210.91958 48.927734 213.15234 50.855474 213.15234 53.240234 C 213.15234 55.624994 210.91958 57.603516 208.12891 57.603516 C 205.38897 57.603516 203.15625 55.624994 203.15625 53.240234 C 203.15625 50.855474 205.38897 48.927734 208.12891 48.927734 z M 194.90039 53.138672 L 194.90039 61.15625 L 198.85938 61.15625 L 198.85938 67.447266 L 194.90039 67.447266 L 194.90039 79.726562 C 194.90039 81.756152 195.61054 82.517578 197.03125 82.517578 C 197.7416 82.517578 198.09828 82.415768 198.85938 82.111328 L 198.85938 88.046875 C 198.14902 88.452785 196.47277 89.011719 194.24023 89.011719 C 188.10074 89.011719 185.25977 85.257843 185.25977 80.539062 L 185.25977 67.447266 L 182.41797 67.447266 L 182.41797 61.15625 L 185.25977 61.15625 L 185.25977 55.574219 L 194.90039 53.138672 z M 166.6875 57.09375 L 169.6875 57.09375 L 169.6875 71.09375 L 166.6875 71.09375 L 166.6875 57.09375 z M 92.119141 60.648438 C 100.59265 60.648438 106.32617 65.164126 106.32617 74.753906 C 106.32617 83.379636 101.30281 88.859375 94.25 88.859375 C 92.52486 88.859375 91.102928 88.656085 90.392578 88.453125 L 90.392578 99.414062 L 80.751953 99.414062 L 80.751953 62.728516 C 83.339673 61.510766 86.842221 60.648435 92.119141 60.648438 z M 141.98242 60.648438 C 150.55741 60.648438 154.61678 66.583801 154.10938 75.869141 L 138.17773 78.103516 C 138.78661 81.046406 140.35834 82.517578 143.85938 82.517578 C 147.1067 82.517578 149.64383 81.806022 151.16602 81.044922 L 153.29688 86.931641 C 150.91212 88.098651 147.71654 89.011719 142.64258 89.011719 C 133.71241 89.011719 128.99414 82.973726 128.99414 74.753906 C 128.99414 66.534096 133.40743 60.648438 141.98242 60.648438 z M 124.06055 60.654297 C 124.95335 60.667874 125.8885 60.69926 126.86523 60.75 L 125.18945 67.447266 C 123.41356 66.584696 121.68841 66.533574 120.41992 66.990234 L 120.41992 88.503906 L 110.7793 88.503906 L 110.7793 62.728516 C 113.57632 61.352202 117.81096 60.559256 124.06055 60.654297 z M 203.30859 61.15625 L 212.94922 61.15625 L 212.94922 88.503906 L 203.30859 88.503906 L 203.30859 61.15625 z M 216.54297 61.15625 L 226.58984 61.15625 L 229.88867 68.005859 L 229.99023 68.005859 L 233.5918 61.15625 L 242.57227 61.15625 L 234.60742 73.789062 L 243.33398 88.503906 L 232.67969 88.503906 L 229.17773 80.943359 L 229.07617 80.943359 L 225.42383 88.503906 L 215.68164 88.503906 L 224.25586 74.398438 L 216.54297 61.15625 z M 141.57617 66.179688 C 138.73475 66.179688 137.16236 68.765636 137.4668 73.535156 L 145.23047 72.369141 C 145.23047 68.208501 144.01167 66.179687 141.57617 66.179688 z M 92.068359 66.279297 C 91.358009 66.279297 90.849228 66.380983 90.392578 66.533203 L 90.392578 82.972656 C 90.747748 83.124866 91.256406 83.226562 91.916016 83.226562 C 95.366316 83.226562 96.787109 80.386048 96.787109 74.804688 C 96.787109 69.071117 95.569389 66.279297 92.068359 66.279297 z M 166.6875 78.09375 L 169.6875 78.09375 L 169.6875 92.09375 L 166.6875 92.09375 L 166.6875 78.09375 z M 166.6875 99.09375 L 169.6875 99.09375 L 169.6875 113.09375 L 166.6875 113.09375 L 166.6875 99.09375 z "
|
||||
transform="translate(257.78125,548.75)"
|
||||
id="rect3888" />
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#3b1c4a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.91138947;marker:none;enable-background:accumulate"
|
||||
d="m 297.38548,404.85558 v 65.16643 c 21.86016,0 39.6016,17.74144 39.6016,39.60159 0,21.86015 -17.74144,39.54187 -39.6016,39.54187 v 65.16644 h 280.37693 v -18.1582 h 5.73417 v 18.1582 h 199.68046 v -65.16644 c -0.02,4e-5 -0.0397,0 -0.0596,0 -21.86015,0 -39.6016,-17.68172 -39.6016,-39.54187 0,-21.86015 17.74145,-39.60159 39.6016,-39.60159 0.02,0 0.0397,-4e-5 0.0596,0 V 404.85558 H 583.49658 v 17.38169 h -5.73417 V 404.85558 Z M 577.76241,435.617 h 5.73417 v 26.75945 h -5.73417 z m 79.21068,24.53074 c 5.33405,0 9.60172,3.68466 9.60172,8.24287 0,4.5582 -4.26767,8.33993 -9.60172,8.33993 -5.2371,0 -9.50469,-3.78173 -9.50469,-8.33993 0,-4.55821 4.26759,-8.24287 9.50469,-8.24287 z m -25.28486,8.04874 v 15.32472 h 7.56717 v 12.02458 h -7.56717 v 23.47051 c 0,3.87934 1.35737,5.33473 4.0729,5.33473 1.35775,0 2.03951,-0.19461 3.49427,-0.77651 v 11.34514 c -1.35777,0.77585 -4.56174,1.84419 -8.829,1.84419 -11.73495,0 -17.16515,-7.17511 -17.16515,-16.19455 v -25.02351 h -5.43179 V 483.5212 h 5.43179 v -10.66944 z m -53.92582,7.5597 h 5.73417 v 26.75945 h -5.73417 z m -142.52917,6.79439 c 16.19618,0 27.15517,8.63124 27.15517,26.96104 0,16.48713 -9.6016,26.96105 -23.08227,26.96105 -3.29741,0 -6.01528,-0.38857 -7.37304,-0.77651 v 20.95062 H 413.50612 V 486.5264 c 4.94614,-2.32759 11.64087,-3.97583 21.72712,-3.97583 z m 95.30815,0 c 16.39014,0 24.14917,11.34479 23.17933,29.09269 l -30.45158,4.27076 c 1.1638,5.62501 4.16799,8.437 10.85985,8.437 6.20689,0 11.05633,-1.36007 13.96583,-2.81482 l 4.0729,11.2518 c -4.5582,2.23062 -10.6662,3.97584 -20.36451,3.97584 -17.06903,0 -26.08749,-11.54096 -26.08749,-27.25223 0,-15.71126 8.43552,-26.96104 24.82567,-26.96104 z m -34.25568,0.0113 c 1.70649,0.026 3.49392,0.0859 5.36084,0.18292 l -3.20307,12.80108 c -3.39442,-1.6487 -6.69185,-1.74642 -9.11643,-0.87356 v 41.121 h -18.42698 v -49.2668 c 5.3462,-2.63068 13.44024,-4.1463 25.38564,-3.96465 z m 151.47387,0.95943 h 18.42699 v 52.27202 h -18.42699 z m 25.29605,0 h 19.20348 l 6.30535,13.09227 h 0.19412 l 6.884,-13.09227 h 17.16517 l -15.22393,24.14622 16.67986,28.1258 h -20.3645 l -6.69361,-14.45115 h -0.19412 l -6.98104,14.45115 h -18.62112 l 16.38867,-26.96104 z m -143.29075,9.60175 c -5.43106,0 -8.43651,4.94275 -7.85461,14.05916 l 14.8394,-2.22871 c 0,-7.9526 -2.3296,-11.83045 -6.98479,-11.83045 z m -94.6287,0.19038 c -1.35776,0 -2.33024,0.19437 -3.20308,0.48532 v 31.4222 c 0.67888,0.29093 1.65112,0.48531 2.91189,0.48531 6.59487,0 9.31055,-5.42933 9.31055,-16.09748 0,-10.95908 -2.32753,-16.29535 -9.01936,-16.29535 z m 142.62623,22.58194 h 5.73417 v 26.75946 h -5.73417 z m 0,40.13918 h 5.73417 v 26.75946 h -5.73417 z"
|
||||
id="rect3888"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:export-filename="/tmp/LOGO.png"
|
||||
inkscape:export-xdpi="88"
|
||||
inkscape:export-ydpi="88" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -1 +1 @@
|
||||
__version__ = "1.10.0"
|
||||
__version__ = "1.13.1"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
|
||||
|
||||
class EventPermission(BasePermission):
|
||||
@@ -24,16 +23,13 @@ class EventPermission(BasePermission):
|
||||
required_permission = None
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
|
||||
last_used = request.session.get('pretix_auth_last_used', time.time())
|
||||
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
|
||||
logout(request)
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
return False
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
return False
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
||||
else request.user)
|
||||
|
||||
@@ -12,7 +12,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count')
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -16,11 +19,44 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
'position', 'default_price', 'price')
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price')
|
||||
|
||||
|
||||
class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
fields = ('addon_category', 'min_count', 'max_count',
|
||||
'position')
|
||||
'position', 'price_included')
|
||||
|
||||
|
||||
class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
fields = ('id', 'addon_category', 'min_count', 'max_count',
|
||||
'position', 'price_included')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
ItemAddOn.clean_max_min_count(data.get('max_count'), data.get('min_count'))
|
||||
|
||||
return data
|
||||
|
||||
def validate_min_count(self, value):
|
||||
ItemAddOn.clean_min_count(value)
|
||||
return value
|
||||
|
||||
def validate_max_count(self, value):
|
||||
ItemAddOn.clean_max_count(value)
|
||||
return value
|
||||
|
||||
def validate_addon_category(self, value):
|
||||
ItemAddOn.clean_categories(self.context['event'], self.context['item'], self.instance, value)
|
||||
return value
|
||||
|
||||
|
||||
class ItemTaxRateField(serializers.Field):
|
||||
@@ -32,8 +68,8 @@ class ItemTaxRateField(serializers.Field):
|
||||
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True)
|
||||
variations = InlineItemVariationSerializer(many=True)
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -44,6 +80,55 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||
'variations', 'addons')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {"has_variations": self.kwargs['has_variations']}
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||
|
||||
return data
|
||||
|
||||
def validate_category(self, value):
|
||||
Item.clean_category(value, self.context['event'])
|
||||
return value
|
||||
|
||||
def validate_tax_rule(self, value):
|
||||
Item.clean_tax_rule(value, self.context['event'])
|
||||
return value
|
||||
|
||||
def validate_variations(self, value):
|
||||
if self.instance is not None:
|
||||
raise ValidationError(_('Updating variations via PATCH/PUT is not supported. Please use the dedicated'
|
||||
' nested endpoint.'))
|
||||
return value
|
||||
|
||||
def validate_addons(self, value):
|
||||
if self.instance is not None:
|
||||
raise ValidationError(_('Updating add-ons via PATCH/PUT is not supported. Please use the dedicated'
|
||||
' nested endpoint.'))
|
||||
else:
|
||||
for addon_data in value:
|
||||
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
|
||||
ItemAddOn.clean_min_count(addon_data['min_count'])
|
||||
ItemAddOn.clean_max_count(addon_data['max_count'])
|
||||
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||
item = Item.objects.create(**validated_data)
|
||||
for variation_data in variations_data:
|
||||
ItemVariation.objects.create(item=item, **variation_data)
|
||||
for addon_data in addons_data:
|
||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||
return item
|
||||
|
||||
|
||||
class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||
@@ -65,7 +150,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position')
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin')
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -38,7 +38,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('datetime',)
|
||||
fields = ('datetime', 'list')
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
@@ -135,7 +135,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
|
||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value', 'checkin_attention')
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -26,6 +26,13 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||
|
||||
item_router = routers.DefaultRouter()
|
||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
||||
|
||||
# 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'):
|
||||
@@ -36,4 +43,7 @@ urlpatterns = [
|
||||
url(r'^', include(router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||
include(checkinlist_router.urls)),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
|
||||
class RichOrderingFilter(OrderingFilter):
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
ordering = self.get_ordering(request, queryset, view)
|
||||
|
||||
if ordering:
|
||||
if hasattr(view, 'ordering_custom'):
|
||||
newo = []
|
||||
for ordering_part in ordering:
|
||||
ob = view.ordering_custom.get(ordering_part)
|
||||
if ob:
|
||||
ob = dict(ob)
|
||||
newo.append(ob.pop('_order'))
|
||||
queryset = queryset.annotate(**ob)
|
||||
else:
|
||||
newo.append(ordering_part)
|
||||
ordering = newo
|
||||
return queryset.order_by(*ordering)
|
||||
|
||||
return queryset
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import django_filters
|
||||
from django.db.models import F, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
|
||||
from pretix.api.serializers.checkin import CheckinListSerializer
|
||||
from pretix.base.models import CheckinList
|
||||
from pretix.api.serializers.order import OrderPositionSerializer
|
||||
from pretix.api.views import RichOrderingFilter
|
||||
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.database import FixedOrderBy
|
||||
|
||||
|
||||
class CheckinListFilter(FilterSet):
|
||||
@@ -57,3 +65,79 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name=value) | Q(addon_to__attendee_name=value))
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'has_checkin', 'addon_to', 'subevent']
|
||||
|
||||
|
||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name', 'positionid')
|
||||
ordering_fields = (
|
||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||
'last_checked_in', 'order__email',
|
||||
)
|
||||
ordering_custom = {
|
||||
'attendee_name': {
|
||||
'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
||||
},
|
||||
'-attendee_name': {
|
||||
'_order': F('display_name').desc(nulls_last=True),
|
||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
||||
},
|
||||
'last_checked_in': {
|
||||
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
|
||||
},
|
||||
'-last_checked_in': {
|
||||
'_order': FixedOrderBy(F('last_checked_in'), nulls_last=True, descending=True),
|
||||
},
|
||||
}
|
||||
|
||||
filter_class = OrderPositionFilter
|
||||
permission = 'can_view_orders'
|
||||
|
||||
@cached_property
|
||||
def checkinlist(self):
|
||||
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
|
||||
|
||||
def get_queryset(self):
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=self.checkinlist.pk
|
||||
).order_by().values('position_id').annotate(
|
||||
m=Max('datetime')
|
||||
).values('m')
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID],
|
||||
subevent=self.checkinlist.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
|
||||
)
|
||||
).select_related('item', 'variation', 'order', 'addon_to')
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||
|
||||
return qs
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.item import (
|
||||
ItemCategorySerializer, ItemSerializer, QuestionSerializer,
|
||||
QuotaSerializer,
|
||||
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
|
||||
ItemVariationSerializer, QuestionSerializer, QuotaSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, Quota,
|
||||
)
|
||||
from pretix.base.models import Item, ItemCategory, Question, Quota
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
class ItemFilter(FilterSet):
|
||||
@@ -28,7 +33,7 @@ class ItemFilter(FilterSet):
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
|
||||
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class ItemViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemSerializer
|
||||
queryset = Item.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -36,10 +41,159 @@ class ItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering = ('position', 'id')
|
||||
filter_class = ItemFilter
|
||||
permission = 'can_change_items'
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['has_variations'] = self.request.data.get('has_variations')
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.changed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
|
||||
'by a user or currently is in a users\'s cart. Please set the item as '
|
||||
'"inactive" instead.')
|
||||
|
||||
instance.log_action(
|
||||
'pretix.event.item.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemVariationSerializer
|
||||
queryset = ItemVariation.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return item.variations.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
if not item.has_variations:
|
||||
raise PermissionDenied('This variation cannot be created because the item does not have variations. '
|
||||
'Changing a product without variations to a product with variations is not allowed.')
|
||||
serializer.save(item=item)
|
||||
item.log_action(
|
||||
'pretix.event.item.variation.added',
|
||||
user=self.request.user,
|
||||
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})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
'pretix.event.item.variation.changed',
|
||||
user=self.request.user,
|
||||
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})
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
|
||||
'by a user or currently is in a users\'s cart. Please set the variation as '
|
||||
'\'inactive\' instead.')
|
||||
if instance.is_only_variation():
|
||||
raise PermissionDenied('This variation cannot be deleted because it is the only variation. Changing a '
|
||||
'product with variations to a product without variations is not allowed.')
|
||||
super().perform_destroy(instance)
|
||||
instance.item.log_action(
|
||||
'pretix.event.item.variation.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data={
|
||||
'value': instance.value,
|
||||
'id': self.kwargs['pk']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemAddOnSerializer
|
||||
queryset = ItemAddOn.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return item.addons.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
||||
serializer.save(base_item=item, addon_category=category)
|
||||
item.log_action(
|
||||
'pretix.event.item.addons.added',
|
||||
user=self.request.user,
|
||||
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})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
'pretix.event.item.addons.changed',
|
||||
user=self.request.user,
|
||||
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})
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
'pretix.event.item.addons.removed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data={'category': instance.addon_category.pk}
|
||||
)
|
||||
|
||||
|
||||
class ItemCategoryFilter(FilterSet):
|
||||
class Meta:
|
||||
|
||||
@@ -21,7 +21,8 @@ from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.invoices import invoice_pdf
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderError, cancel_order, extend_order, mark_order_paid,
|
||||
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,
|
||||
@@ -109,9 +110,9 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_PENDING:
|
||||
if not order.cancel_allowed():
|
||||
return Response(
|
||||
{'detail': 'The order is not pending.'},
|
||||
{'detail': 'The order is not allowed to be canceled.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@@ -153,10 +154,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_EXPIRED
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.expired',
|
||||
mark_order_expired(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
|
||||
@@ -11,7 +11,8 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import payment # NOQA
|
||||
from . import exporters # NOQA
|
||||
from . import invoice # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA
|
||||
from . import notifications # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
def round_decimal(dec):
|
||||
|
||||
def round_decimal(dec, currency=None):
|
||||
if currency:
|
||||
places = settings.CURRENCY_PLACES.get(currency, 2)
|
||||
return Decimal(dec).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
return Decimal(dec).quantize(Decimal('0.01'), ROUND_HALF_UP)
|
||||
|
||||
@@ -25,7 +25,7 @@ class BaseExporter:
|
||||
"""
|
||||
A short and unique identifier for this exporter.
|
||||
This should only contain lowercase letters and in most
|
||||
cases will be the same as your packagename.
|
||||
cases will be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class AnswerFilesExporter(BaseExporter):
|
||||
i.file.close()
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return 'answers.zip', 'application/zip', zipf.read()
|
||||
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_answers")
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import os
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from zipfile import ZipFile
|
||||
|
||||
import dateutil.parser
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -15,9 +18,26 @@ class InvoiceExporter(BaseExporter):
|
||||
verbose_name = _('All invoices')
|
||||
|
||||
def render(self, form_data: dict):
|
||||
qs = self.event.invoices.all()
|
||||
|
||||
if form_data.get('payment_provider'):
|
||||
qs = qs.filter(order__payment_provider=form_data.get('payment_provider'))
|
||||
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(date__gte=date_value)
|
||||
|
||||
if form_data.get('date_to'):
|
||||
date_value = form_data.get('date_to')
|
||||
if isinstance(date_value, str):
|
||||
date_value = dateutil.parser.parse(date_value).date()
|
||||
qs = qs.filter(date__lte=date_value)
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for i in self.event.invoices.all():
|
||||
for i in qs:
|
||||
if not i.file:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
@@ -26,7 +46,44 @@ class InvoiceExporter(BaseExporter):
|
||||
i.file.close()
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return 'invoices.zip', 'application/zip', zipf.read()
|
||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=_('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=_('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||
'not always correspond to the order or payment date.')
|
||||
)),
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=_('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=_('Only include invoices issued on or before this date. Note that the invoice date '
|
||||
'does not always correspond to the order or payment date.')
|
||||
)),
|
||||
('payment_provider',
|
||||
forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
choices=[
|
||||
('', _('All payment providers')),
|
||||
] + [
|
||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||
],
|
||||
required=False,
|
||||
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.')
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_invoices")
|
||||
|
||||
@@ -101,7 +101,7 @@ class JSONExporter(BaseExporter):
|
||||
}
|
||||
}
|
||||
|
||||
return 'pretixdata.json', 'application/json', json.dumps(jo, cls=DjangoJSONEncoder)
|
||||
return '{}_pretixdata.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder)
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_json")
|
||||
|
||||
@@ -21,9 +21,9 @@ class MailExporter(BaseExporter):
|
||||
pos = OrderPosition.objects.filter(
|
||||
order__event=self.event, order__status__in=form_data['status']
|
||||
).values('attendee_email')
|
||||
data = "\r\n".join(set(a['email'] for a in addrs)
|
||||
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
|
||||
| set(a['attendee_email'] for a in pos if a['attendee_email']))
|
||||
return 'pretixemails.txt', 'text/plain', data.encode("utf-8")
|
||||
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
|
||||
@@ -140,7 +140,7 @@ class OrderListExporter(BaseExporter):
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
writer.writerow(row)
|
||||
|
||||
return 'orders.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
class QuotaListExporter(BaseExporter):
|
||||
@@ -171,7 +171,7 @@ class QuotaListExporter(BaseExporter):
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
return 'quotas.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
return '{}_quotas.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
||||
|
||||
@@ -11,16 +11,16 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
from .validators import PlaceholderValidator # NOQA
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
||||
# compatibility shim for django-i18nfield library
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event', None)
|
||||
if event:
|
||||
kwargs['locales'] = event.settings.get('locales')
|
||||
self.event = kwargs.pop('event', None)
|
||||
if self.event:
|
||||
kwargs['locales'] = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ class I18nFormSet(i18nfield.forms.I18nModelFormSet):
|
||||
# compatibility shim for django-i18nfield library
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event', None)
|
||||
if event:
|
||||
kwargs['locales'] = event.settings.get('locales')
|
||||
self.event = kwargs.pop('event', None)
|
||||
if self.event:
|
||||
kwargs['locales'] = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
244
src/pretix/base/forms/questions.py
Normal file
244
src/pretix/base/forms/questions.py
Normal file
@@ -0,0 +1,244 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
TimePickerWidget, UploadedFileWidget,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, Question
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||||
as well as additional questions defined by the organizer.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Takes two additional keyword arguments:
|
||||
|
||||
:param cartpos: The cart position the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
cartpos = self.cartpos = kwargs.pop('cartpos', None)
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
item = pos.item
|
||||
questions = pos.item.questions_to_ask
|
||||
event = kwargs.pop('event')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if item.admission and event.settings.attendee_names_asked:
|
||||
self.fields['attendee_name'] = forms.CharField(
|
||||
max_length=255, required=event.settings.attendee_names_required,
|
||||
label=_('Attendee name'),
|
||||
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name),
|
||||
)
|
||||
if item.admission and event.settings.attendee_emails_asked:
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
|
||||
)
|
||||
|
||||
for q in questions:
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
answers = [a for a in pos.answerlist if a.question_id == q.id]
|
||||
if answers:
|
||||
initial = answers[0]
|
||||
else:
|
||||
initial = None
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if q.required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
widget = forms.CheckboxInput(attrs={'required': 'required'})
|
||||
else:
|
||||
widget = forms.CheckboxInput()
|
||||
|
||||
if initial:
|
||||
initialbool = (initial.answer == "True")
|
||||
else:
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
min_value=Decimal('0.00'),
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=q.options,
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
widget=forms.Select,
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=q.options,
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
field = forms.FileField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
field = forms.DateField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
|
||||
widget=DatePickerWidget(),
|
||||
)
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
field = forms.TimeField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
field = forms.SplitDateTimeField(
|
||||
label=q.question, required=q.required,
|
||||
help_text=q.help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
self.fields['question_%s' % q.id] = field
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
data = pos.meta_info_data
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
|
||||
class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
vat_warning = False
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'internal_reference')
|
||||
widgets = {
|
||||
'is_business': BusinessBooleanRadio,
|
||||
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||||
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'name': forms.TextInput(attrs={}),
|
||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'internal_reference': forms.TextInput,
|
||||
}
|
||||
labels = {
|
||||
'is_business': ''
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = event = kwargs.pop('event')
|
||||
self.request = kwargs.pop('request', None)
|
||||
self.validate_vat_id = kwargs.pop('validate_vat_id')
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
del self.fields['vat_id']
|
||||
if not event.settings.invoice_address_required:
|
||||
for k, f in self.fields.items():
|
||||
f.required = False
|
||||
f.widget.is_required = False
|
||||
if 'required' in f.widget.attrs:
|
||||
del f.widget.attrs['required']
|
||||
|
||||
if event.settings.invoice_name_required:
|
||||
self.fields['name'].required = True
|
||||
else:
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if not data.get('name') and not data.get('company') and self.event.settings.invoice_address_required:
|
||||
raise ValidationError(_('You need to provide either a company name or your name.'))
|
||||
|
||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||
self.instance.vat_id_validated = False
|
||||
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
pass
|
||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||
if data.get('vat_id')[:2] != str(data.get('country')):
|
||||
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||
try:
|
||||
result = vat_moss.id.validate(data.get('vat_id'))
|
||||
if result:
|
||||
country_code, normalized_id, company_name = result
|
||||
self.instance.vat_id_validated = True
|
||||
self.instance.vat_id = normalized_id
|
||||
except vat_moss.errors.InvalidError:
|
||||
raise ValidationError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||
except vat_moss.errors.WebServiceUnavailableError:
|
||||
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
|
||||
'your country is currently not available. We will therefore '
|
||||
'need to charge VAT on your invoice. You can get the tax amount '
|
||||
'back via the VAT reimbursement process.'))
|
||||
except vat_moss.errors.WebServiceError:
|
||||
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
|
||||
'your country returned an incorrect result. We will therefore '
|
||||
'need to charge VAT on your invoice. Please contact support to '
|
||||
'resolve this manually.'))
|
||||
else:
|
||||
self.instance.vat_id_validated = False
|
||||
@@ -23,6 +23,12 @@ class PlaceholderValidator(BaseValidator):
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
if value.count('{') != value.count('}'):
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[\w\s]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
|
||||
135
src/pretix/base/forms/widgets.py
Normal file
135
src/pretix/base/forms/widgets.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
|
||||
class DatePickerWidget(forms.DateInput):
|
||||
def __init__(self, attrs=None, date_format=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
date_attrs = dict(attrs)
|
||||
date_attrs.setdefault('class', 'form-control')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
date_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
forms.DateInput.__init__(self, date_attrs, date_format)
|
||||
|
||||
|
||||
class TimePickerWidget(forms.TimeInput):
|
||||
def __init__(self, attrs=None, time_format=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
time_attrs = dict(attrs)
|
||||
time_attrs.setdefault('class', 'form-control')
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
time_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
forms.TimeInput.__init__(self, time_attrs, time_format)
|
||||
|
||||
|
||||
class UploadedFileWidget(forms.ClearableFileInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.position = kwargs.pop('position')
|
||||
self.event = kwargs.pop('event')
|
||||
self.answer = kwargs.pop('answer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class FakeFile:
|
||||
def __init__(self, file, position, event, answer):
|
||||
self.file = file
|
||||
self.position = position
|
||||
self.event = event
|
||||
self.answer = answer
|
||||
|
||||
def __str__(self):
|
||||
return os.path.basename(self.file.name).split('.', 1)[-1]
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if isinstance(self.position, OrderPosition):
|
||||
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.position.order.code,
|
||||
'secret': self.position.order.secret,
|
||||
'answer': self.answer.pk,
|
||||
})
|
||||
else:
|
||||
return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
|
||||
'answer': self.answer.pk,
|
||||
})
|
||||
|
||||
def format_value(self, value):
|
||||
if self.is_initial(value):
|
||||
return self.FakeFile(value, self.position, self.event, self.answer)
|
||||
|
||||
|
||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
||||
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
date_attrs = dict(attrs)
|
||||
time_attrs = dict(attrs)
|
||||
date_attrs.setdefault('class', 'form-control splitdatetimepart')
|
||||
time_attrs.setdefault('class', 'form-control splitdatetimepart')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
date_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
time_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
widgets = (
|
||||
forms.DateInput(attrs=date_attrs, format=date_format),
|
||||
forms.TimeInput(attrs=time_attrs, format=time_format),
|
||||
)
|
||||
# Skip one hierarchy level
|
||||
forms.MultiWidget.__init__(self, widgets, attrs)
|
||||
|
||||
|
||||
class BusinessBooleanRadio(forms.RadioSelect):
|
||||
def __init__(self, attrs=None):
|
||||
choices = (
|
||||
('individual', _('Individual customer')),
|
||||
('business', _('Business customer')),
|
||||
)
|
||||
super().__init__(attrs, choices)
|
||||
|
||||
def format_value(self, value):
|
||||
try:
|
||||
return {True: 'business', False: 'individual'}[value]
|
||||
except KeyError:
|
||||
return 'individual'
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = data.get(name)
|
||||
return {
|
||||
'business': True,
|
||||
True: True,
|
||||
'True': True,
|
||||
'individual': False,
|
||||
'False': False,
|
||||
False: False,
|
||||
}.get(value)
|
||||
@@ -12,6 +12,8 @@ from i18nfield.forms import I18nFormField # noqa
|
||||
from i18nfield.strings import LazyI18nString # noqa
|
||||
from i18nfield.utils import I18nJSONEncoder # noqa
|
||||
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class LazyDate:
|
||||
def __init__(self, value):
|
||||
@@ -24,6 +26,18 @@ class LazyDate:
|
||||
return date_format(self.value, "SHORT_DATE_FORMAT")
|
||||
|
||||
|
||||
class LazyCurrencyNumber:
|
||||
def __init__(self, value, currency):
|
||||
self.value = value
|
||||
self.currency = currency
|
||||
|
||||
def __format__(self, format_spec):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return money_filter(self.value, self.currency)
|
||||
|
||||
|
||||
class LazyNumber:
|
||||
def __init__(self, value, decimal_pos=2):
|
||||
self.value = value
|
||||
|
||||
@@ -24,6 +24,7 @@ from reportlab.platypus import (
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class BaseInvoiceRenderer:
|
||||
@@ -321,6 +322,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
]
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(pgettext('invoice', 'Invoice')
|
||||
@@ -352,28 +355,52 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
('LEFTPADDING', (0, 0), (0, -1), 0),
|
||||
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
|
||||
]
|
||||
tdata = [(
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net'),
|
||||
pgettext('invoice', 'Gross'),
|
||||
)]
|
||||
if has_taxes:
|
||||
tdata = [(
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Qty'),
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net'),
|
||||
pgettext('invoice', 'Gross'),
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Qty'),
|
||||
pgettext('invoice', 'Amount'),
|
||||
)]
|
||||
|
||||
total = Decimal('0.00')
|
||||
for line in self.invoice.lines.all():
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
localize(line.tax_rate) + " %",
|
||||
localize(line.net_value) + " " + self.invoice.event.currency,
|
||||
localize(line.gross_value) + " " + self.invoice.event.currency,
|
||||
))
|
||||
if has_taxes:
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
"1",
|
||||
localize(line.tax_rate) + " %",
|
||||
money_filter(line.net_value, self.invoice.event.currency),
|
||||
money_filter(line.gross_value, self.invoice.event.currency),
|
||||
))
|
||||
else:
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
"1",
|
||||
money_filter(line.gross_value, self.invoice.event.currency),
|
||||
))
|
||||
taxvalue_map[line.tax_rate, line.tax_name] += line.tax_value
|
||||
grossvalue_map[line.tax_rate, line.tax_name] += line.gross_value
|
||||
total += line.gross_value
|
||||
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + self.invoice.event.currency
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||
else:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .05, .30)]
|
||||
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(table)
|
||||
@@ -410,9 +437,9 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
tax = taxvalue_map[idx]
|
||||
tdata.append([
|
||||
localize(rate) + " % " + name,
|
||||
localize(gross - tax) + " " + self.invoice.event.currency,
|
||||
localize(gross) + " " + self.invoice.event.currency,
|
||||
localize(tax) + " " + self.invoice.event.currency,
|
||||
money_filter(gross - tax, self.invoice.event.currency),
|
||||
money_filter(gross, self.invoice.event.currency),
|
||||
money_filter(tax, self.invoice.event.currency),
|
||||
''
|
||||
])
|
||||
|
||||
@@ -422,7 +449,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
except ValueError:
|
||||
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
||||
|
||||
if len(tdata) > 1:
|
||||
if len(tdata) > 1 and has_taxes:
|
||||
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.core.urlresolvers import get_script_prefix
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||
from django.utils.translation.trans_real import (
|
||||
@@ -166,9 +165,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'/api/v1/docs/',
|
||||
)
|
||||
|
||||
def process_request(self, request):
|
||||
request.csp_nonce = get_random_string(length=32)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
@@ -183,7 +179,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
|
||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'style-src': ["{static}", "{media}", "'nonce-{nonce}'"],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
'font-src': ["{static}"],
|
||||
@@ -191,7 +187,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||
# this. However, we'll restrict it to HTTPS.
|
||||
'form-action': ["{dynamic}", "https:"],
|
||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
||||
'report-uri': ["/csp_report/"],
|
||||
}
|
||||
if 'Content-Security-Policy' in resp:
|
||||
@@ -222,10 +218,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
|
||||
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain, nonce=request.csp_nonce)
|
||||
media=mediadomain)
|
||||
for k, v in h.items():
|
||||
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain,
|
||||
nonce=request.csp_nonce).split(' ')
|
||||
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')
|
||||
resp['Content-Security-Policy'] = _render_csp(h)
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
59
src/pretix/base/migrations/0078_auto_20171206_1603.py
Normal file
59
src/pretix/base/migrations/0078_auto_20171206_1603.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-12-06 16:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.auth
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0077_auto_20171124_1629'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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=set([('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),
|
||||
),
|
||||
]
|
||||
39
src/pretix/base/migrations/0079_auto_20180115_0855.py
Normal file
39
src/pretix/base/migrations/0079_auto_20180115_0855.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-15 08:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
|
||||
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'))
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0078_auto_20171206_1603'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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(
|
||||
set_full_invoice_no,
|
||||
migrations.RunPython.noop
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-15 14:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0079_auto_20180115_0855'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='ask_during_checkin',
|
||||
field=models.BooleanField(default=False, help_text='Supported by pretixdroid 1.8 and newer or pretixdesk 0.2 and newer.', verbose_name='Ask during check-in instead of during registration'),
|
||||
),
|
||||
]
|
||||
46
src/pretix/base/migrations/0081_auto_20180220_1031.py
Normal file
46
src/pretix/base/migrations/0081_auto_20180220_1031.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-20 10:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0080_question_ask_during_checkin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='include_pending',
|
||||
field=models.BooleanField(default=False, 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='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.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'),
|
||||
),
|
||||
]
|
||||
25
src/pretix/base/migrations/0082_auto_20180222_0938.py
Normal file
25
src/pretix/base/migrations/0082_auto_20180222_0938.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-22 09:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0081_auto_20180220_1031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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.AlterField(
|
||||
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'),
|
||||
),
|
||||
]
|
||||
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-28 21:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0082_auto_20180222_0938'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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),
|
||||
),
|
||||
]
|
||||
41
src/pretix/base/migrations/0084_questionoption_position.py
Normal file
41
src/pretix/base/migrations/0084_questionoption_position.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-03-03 16:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0083_auto_20180228_2102'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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(
|
||||
set_position,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,7 @@ from .items import (
|
||||
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .notifications import NotificationSetting
|
||||
from .orders import (
|
||||
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
|
||||
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
|
||||
|
||||
@@ -4,9 +4,11 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||
)
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
@@ -40,6 +42,10 @@ class UserManager(BaseUserManager):
|
||||
return user
|
||||
|
||||
|
||||
def generate_notifications_token():
|
||||
return get_random_string(length=32)
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
"""
|
||||
This is the user model used by pretix for authentication.
|
||||
@@ -80,7 +86,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
timezone = models.CharField(max_length=100,
|
||||
default=settings.TIME_ZONE,
|
||||
verbose_name=_('Timezone'))
|
||||
require_2fa = models.BooleanField(default=False)
|
||||
require_2fa = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Two-factor authentification is required to log in')
|
||||
)
|
||||
notifications_send = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_('Receive notifications according to my settings below'),
|
||||
help_text=_('If turned off, you will not get any notifications.')
|
||||
)
|
||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@@ -147,6 +162,19 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
def send_password_reset(self):
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
mail(
|
||||
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||
{
|
||||
'user': self,
|
||||
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||
},
|
||||
None, locale=self.locale
|
||||
)
|
||||
|
||||
@property
|
||||
def all_logentries(self):
|
||||
from pretix.base.models import LogEntry
|
||||
|
||||
@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
|
||||
|
||||
class LoggingMixin:
|
||||
|
||||
def log_action(self, action, data=None, user=None, api_token=None):
|
||||
def log_action(self, action, data=None, user=None, api_token=None, save=True):
|
||||
"""
|
||||
Create a LogEntry object that is related to this object.
|
||||
See the LogEntry documentation for details.
|
||||
@@ -47,6 +47,8 @@ class LoggingMixin:
|
||||
"""
|
||||
from .log import LogEntry
|
||||
from .event import Event
|
||||
from ..notifications import get_all_notification_types
|
||||
from ..services.notifications import notify
|
||||
|
||||
event = None
|
||||
if isinstance(self, Event):
|
||||
@@ -58,7 +60,12 @@ class LoggingMixin:
|
||||
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)
|
||||
logentry.save()
|
||||
if save:
|
||||
logentry.save()
|
||||
|
||||
if action in get_all_notification_types():
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
return logentry
|
||||
|
||||
|
||||
class LoggedModel(models.Model, LoggingMixin):
|
||||
|
||||
@@ -14,11 +14,27 @@ class CheckinList(LoggedModel):
|
||||
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'))
|
||||
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 '
|
||||
'order have not been paid. This only works with pretixdesk '
|
||||
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
|
||||
|
||||
@staticmethod
|
||||
def annotate_with_numbers(qs, event):
|
||||
from . import Order, OrderPosition
|
||||
cqs = Checkin.objects.filter(
|
||||
"""
|
||||
Modifies a queryset of checkin lists by annotating it with the number of order positions and
|
||||
checkins associated with it.
|
||||
"""
|
||||
# Import here to prevent circular import
|
||||
from . import Order, OrderPosition, Item
|
||||
|
||||
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
|
||||
# First, we prepare a subquery that for every check-in that belongs to a paid-order
|
||||
# position and to the list in question. Then, we check that it also belongs to the
|
||||
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
|
||||
# since we filtered by lists).
|
||||
cqs_paid = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status=Order.STATUS_PAID,
|
||||
list=OuterRef('pk')
|
||||
@@ -30,7 +46,24 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_all = OrderPosition.objects.filter(
|
||||
cqs_paid_and_pending = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
list=OuterRef('pk')
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(position__subevent=OuterRef('subevent'))
|
||||
| (Q(position__subevent__isnull=True))
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now for the hard part: getting all order positions that contribute to this list. This
|
||||
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
|
||||
# lists that contain all the products of the event. This is the simpler one, it basically
|
||||
# looks like the check-in counter above.
|
||||
pqs_all_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
).filter(
|
||||
@@ -41,10 +74,9 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_limited = OrderPosition.objects.filter(
|
||||
pqs_all_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
item__in=OuterRef('limit_products')
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
@@ -54,13 +86,61 @@ class CheckinList(LoggedModel):
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now we need a subquery for the case of checkin lists that are limited to certain
|
||||
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
|
||||
# with the products table and we'd get duplicate rows in the output with different annotations
|
||||
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
|
||||
# to retrieve all of those items and then check if the item_id is IN this subquery result.
|
||||
pqs_limited_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
|
||||
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
|
||||
# we want to display a progress bar.
|
||||
return qs.annotate(
|
||||
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
|
||||
position_count=Coalesce(Case(
|
||||
When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())),
|
||||
default=Subquery(pqs_limited, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
), 0)
|
||||
checkin_count=Coalesce(
|
||||
Case(
|
||||
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
|
||||
default=Subquery(cqs_paid, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
),
|
||||
position_count=Coalesce(
|
||||
Case(
|
||||
When(all_products=True, include_pending=False,
|
||||
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
|
||||
When(all_products=True, include_pending=True,
|
||||
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
|
||||
When(all_products=False, include_pending=False,
|
||||
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
|
||||
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
)
|
||||
).annotate(
|
||||
percent=Case(
|
||||
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
|
||||
@@ -75,7 +155,7 @@ class CheckinList(LoggedModel):
|
||||
|
||||
class Checkin(models.Model):
|
||||
"""
|
||||
A checkin object is created when a person enters the event.
|
||||
A check-in object is created when a person enters the event.
|
||||
"""
|
||||
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
|
||||
datetime = models.DateTimeField(default=now)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -26,7 +26,7 @@ from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.json import safe_string
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .organizer import Organizer
|
||||
from .organizer import Organizer, Team
|
||||
|
||||
|
||||
class EventMixin:
|
||||
@@ -43,7 +43,7 @@ class EventMixin:
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
@@ -55,7 +55,7 @@ class EventMixin:
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
if ``show_date_to`` is ``False``.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
@@ -68,7 +68,7 @@ class EventMixin:
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
@@ -79,7 +79,7 @@ class EventMixin:
|
||||
Returns a formatted string containing the start time of the event, ignoring
|
||||
the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
@@ -90,7 +90,7 @@ class EventMixin:
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
if ``show_date_to`` is ``False``.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
@@ -100,23 +100,30 @@ class EventMixin:
|
||||
|
||||
def get_date_range_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the event date
|
||||
Returns a formatted string containing the start date and the end date
|
||||
of the event with respect to the current locale and to the ``show_times`` and
|
||||
``show_date_to`` settings.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
|
||||
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz.timezone(self.settings.timezone)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
"""
|
||||
Is true, when ``presale_end`` is set and in the past.
|
||||
"""
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return True
|
||||
return False
|
||||
if self.presale_end:
|
||||
return now() > self.presale_end
|
||||
elif self.date_to:
|
||||
return now() > self.date_to
|
||||
else:
|
||||
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||
|
||||
@property
|
||||
def presale_is_running(self):
|
||||
@@ -126,9 +133,7 @@ class EventMixin:
|
||||
"""
|
||||
if self.presale_start and now() < self.presale_start:
|
||||
return False
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return False
|
||||
return True
|
||||
return not self.presale_has_ended
|
||||
|
||||
@property
|
||||
def event_microdata(self):
|
||||
@@ -229,7 +234,8 @@ class Event(EventMixin, LoggedModel):
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
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."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
@@ -262,6 +268,13 @@ class Event(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
if self.has_subevents:
|
||||
return self.presale_end and now() > self.presale_end
|
||||
else:
|
||||
return super().presale_has_ended
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.cache.clear()
|
||||
@@ -511,6 +524,42 @@ class Event(EventMixin, LoggedModel):
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
def get_users_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of users who have any permission to this event.
|
||||
|
||||
:return: Iterable of User
|
||||
"""
|
||||
return self.get_users_with_permission(None)
|
||||
|
||||
def get_users_with_permission(self, permission):
|
||||
"""
|
||||
Returns a queryset of users who have a specific permission to this event.
|
||||
|
||||
:return: Iterable of User
|
||||
"""
|
||||
from .auth import User
|
||||
|
||||
if permission:
|
||||
kwargs = {permission: True}
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
team_with_perm = Team.objects.filter(
|
||||
members__pk=OuterRef('pk'),
|
||||
organizer=self.organizer,
|
||||
**kwargs
|
||||
).filter(
|
||||
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
||||
)
|
||||
|
||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
|
||||
Q(is_superuser=True) | Q(twp=True)
|
||||
)
|
||||
|
||||
def allow_delete(self):
|
||||
return not self.orders.exists() and not self.invoices.exists()
|
||||
|
||||
|
||||
class SubEvent(EventMixin, LoggedModel):
|
||||
"""
|
||||
@@ -550,7 +599,8 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
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."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
@@ -606,6 +656,13 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
@property
|
||||
def currency(self):
|
||||
return self.event.currency
|
||||
|
||||
def allow_delete(self):
|
||||
return self.event.subevents.count() > 1
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
@@ -665,6 +722,21 @@ class RequiredAction(models.Model):
|
||||
return response
|
||||
return self.action_type
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = not self.pk
|
||||
super().save(*args, **kwargs)
|
||||
if created:
|
||||
from .log import LogEntry
|
||||
from ..services.notifications import notify
|
||||
|
||||
logentry = LogEntry.objects.create(
|
||||
content_object=self,
|
||||
action_type='pretix.event.action_required',
|
||||
event=self.event,
|
||||
visible=False
|
||||
)
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
|
||||
|
||||
class EventMetaProperty(LoggedModel):
|
||||
"""
|
||||
|
||||
@@ -41,6 +41,8 @@ class Invoice(models.Model):
|
||||
:type invoice_from: str
|
||||
:param invoice_to: The receiver address
|
||||
:type invoice_to: str
|
||||
:param full_invoice_no: The full invoice number (for performance reasons only)
|
||||
:type full_invoice_no: str
|
||||
:param date: The invoice date
|
||||
:type date: date
|
||||
:param locale: The locale in which the invoice should be printed
|
||||
@@ -55,9 +57,9 @@ class Invoice(models.Model):
|
||||
:type footer_text: str
|
||||
:param foreign_currency_display: A different currency that taxes should also be displayed in.
|
||||
:type foreign_currency_display: str
|
||||
:param foreign_currency_rate: The rate of a forein currency that the taxes should be displayed in.
|
||||
:param foreign_currency_rate: The rate of a foreign currency that the taxes should be displayed in.
|
||||
:type foreign_currency_rate: Decimal
|
||||
:param foreign_currency_rate_date: The date of the forein currency exchange rates.
|
||||
:param foreign_currency_rate_date: The date of the foreign currency exchange rates.
|
||||
:type foreign_currency_rate_date: date
|
||||
:param file: The filename of the rendered invoice
|
||||
:type file: File
|
||||
@@ -67,6 +69,7 @@ class Invoice(models.Model):
|
||||
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)
|
||||
invoice_from = models.TextField()
|
||||
@@ -80,6 +83,7 @@ class Invoice(models.Model):
|
||||
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
||||
internal_reference = models.TextField(blank=True)
|
||||
|
||||
@@ -122,6 +126,8 @@ class Invoice(models.Model):
|
||||
# Suppress duplicate key errors and try again
|
||||
if i == 9:
|
||||
raise
|
||||
|
||||
self.full_invoice_no = self.prefix + self.invoice_no
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, time
|
||||
from decimal import Decimal, DecimalException
|
||||
from typing import Tuple
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Func, Q, Sum
|
||||
from django.utils import formats
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.timezone import is_naive, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
@@ -179,7 +182,7 @@ class Item(LoggedModel):
|
||||
:type max_per_order: int
|
||||
:param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
|
||||
:type min_per_order: int
|
||||
:param checkin_attention: Requires special attention at checkin
|
||||
:param checkin_attention: Requires special attention at check-in
|
||||
:type checkin_attention: bool
|
||||
"""
|
||||
|
||||
@@ -369,10 +372,41 @@ class Item(LoggedModel):
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import CartPosition, OrderPosition
|
||||
|
||||
return (
|
||||
not OrderPosition.objects.filter(item=self).exists()
|
||||
and not CartPosition.objects.filter(item=self).exists()
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def has_variations(self):
|
||||
return self.variations.exists()
|
||||
|
||||
@staticmethod
|
||||
def clean_per_order(min_per_order, max_per_order):
|
||||
if min_per_order is not None and max_per_order is not None:
|
||||
if min_per_order > max_per_order:
|
||||
raise ValidationError(_('The maximum number per order can not be lower than the minimum number per '
|
||||
'order.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_category(category, event):
|
||||
if category is not None and category.event is not None and category.event != event:
|
||||
raise ValidationError(_('The item\'s category must belong to the same event as the item.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_tax_rule(tax_rule, event):
|
||||
if tax_rule is not None and tax_rule.event is not None and tax_rule.event != event:
|
||||
raise ValidationError(_('The item\'s tax rule must belong to the same event as the item.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_available(from_date, until_date):
|
||||
if from_date is not None and until_date is not None:
|
||||
if from_date > until_date:
|
||||
raise ValidationError(_('The item\'s availability cannot end before it starts.'))
|
||||
|
||||
|
||||
class ItemVariation(models.Model):
|
||||
"""
|
||||
@@ -476,6 +510,17 @@ class ItemVariation(models.Model):
|
||||
return self.id < other.id
|
||||
return self.position < other.position
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import CartPosition, OrderPosition
|
||||
|
||||
return (
|
||||
not OrderPosition.objects.filter(variation=self).exists()
|
||||
and not CartPosition.objects.filter(variation=self).exists()
|
||||
)
|
||||
|
||||
def is_only_variation(self):
|
||||
return ItemVariation.objects.filter(item=self.item).count() == 1
|
||||
|
||||
|
||||
class ItemAddOn(models.Model):
|
||||
"""
|
||||
@@ -527,8 +572,34 @@ class ItemAddOn(models.Model):
|
||||
ordering = ('position', 'pk')
|
||||
|
||||
def clean(self):
|
||||
if self.max_count < self.min_count:
|
||||
raise ValidationError(_('The minimum number needs to be lower than the maximum number.'))
|
||||
self.clean_min_count(self.min_count)
|
||||
self.clean_max_count(self.max_count)
|
||||
self.clean_max_min_count(self.max_count, self.min_count)
|
||||
|
||||
@staticmethod
|
||||
def clean_categories(event, item, addon, new_category):
|
||||
if event != new_category.event:
|
||||
raise ValidationError(_('The add-on\'s category must belong to the same event as the item.'))
|
||||
if item is not None:
|
||||
if addon is None or addon.addon_category != new_category:
|
||||
for addon in item.addons.all():
|
||||
if addon.addon_category == new_category:
|
||||
raise ValidationError(_('The item already has an add-on of this category.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_min_count(min_count):
|
||||
if min_count < 0:
|
||||
raise ValidationError(_('The minimum count needs to be equal to or greater than zero.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_max_count(max_count):
|
||||
if max_count < 0:
|
||||
raise ValidationError(_('The maximum count needs to be equal to or greater than zero.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_max_min_count(max_count, min_count):
|
||||
if max_count < min_count:
|
||||
raise ValidationError(_('The maximum count needs to be greater than the minimum count.'))
|
||||
|
||||
|
||||
class Question(LoggedModel):
|
||||
@@ -543,17 +614,22 @@ class Question(LoggedModel):
|
||||
* a multi-line string (``TYPE_TEXT``)
|
||||
* a boolean (``TYPE_BOOLEAN``)
|
||||
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
|
||||
* a file upload (``TYPE_FILE``))
|
||||
* a file upload (``TYPE_FILE``)
|
||||
* a date (``TYPE_DATE``)
|
||||
* a time (``TYPE_TIME``)
|
||||
* a date and a time (``TYPE_DATETIME``)
|
||||
|
||||
:param event: The event this question belongs to
|
||||
:type event: Event
|
||||
:param question: The question text. This will be displayed next to the input field.
|
||||
:type question: str
|
||||
:param type: One of the above types
|
||||
:param required: Whether answering this question is required for submiting an order including
|
||||
:param required: Whether answering this question is required for submitting an order including
|
||||
items associated with this question.
|
||||
:type required: bool
|
||||
:param items: A set of ``Items`` objects that this question should be applied to
|
||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||
:type ask_during_checkin: bool
|
||||
"""
|
||||
TYPE_NUMBER = "N"
|
||||
TYPE_STRING = "S"
|
||||
@@ -562,6 +638,9 @@ class Question(LoggedModel):
|
||||
TYPE_CHOICE = "C"
|
||||
TYPE_CHOICE_MULTIPLE = "M"
|
||||
TYPE_FILE = "F"
|
||||
TYPE_DATE = "D"
|
||||
TYPE_TIME = "H"
|
||||
TYPE_DATETIME = "W"
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_NUMBER, _("Number")),
|
||||
(TYPE_STRING, _("Text (one line)")),
|
||||
@@ -570,6 +649,9 @@ class Question(LoggedModel):
|
||||
(TYPE_CHOICE, _("Choose one from a list")),
|
||||
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
|
||||
(TYPE_FILE, _("File upload")),
|
||||
(TYPE_DATE, _("Date")),
|
||||
(TYPE_TIME, _("Time")),
|
||||
(TYPE_DATETIME, _("Date and time")),
|
||||
)
|
||||
|
||||
event = models.ForeignKey(
|
||||
@@ -600,8 +682,15 @@ class Question(LoggedModel):
|
||||
blank=True,
|
||||
help_text=_('This question will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.IntegerField(
|
||||
default=0
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('This will only work if you handle your check-in with pretixdroid 1.8 or newer or '
|
||||
'pretixdesk 0.2 or newer.'),
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -629,14 +718,78 @@ class Question(LoggedModel):
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def clean_answer(self, answer):
|
||||
if self.required:
|
||||
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if not answer:
|
||||
if self.type == Question.TYPE_BOOLEAN:
|
||||
return False
|
||||
return None
|
||||
|
||||
if self.type == Question.TYPE_CHOICE:
|
||||
try:
|
||||
return self.options.get(pk=answer)
|
||||
except:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
try:
|
||||
if isinstance(answer, str):
|
||||
return list(self.options.filter(pk__in=answer.split(",")))
|
||||
else:
|
||||
return list(self.options.filter(pk__in=answer))
|
||||
except:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
elif self.type == Question.TYPE_BOOLEAN:
|
||||
return answer in ('true', 'True', True)
|
||||
elif self.type == Question.TYPE_NUMBER:
|
||||
answer = formats.sanitize_separators(answer)
|
||||
answer = str(answer).strip()
|
||||
try:
|
||||
return Decimal(answer)
|
||||
except DecimalException:
|
||||
raise ValidationError(_('Invalid number input.'))
|
||||
elif self.type == Question.TYPE_DATE:
|
||||
if isinstance(answer, date):
|
||||
return answer
|
||||
try:
|
||||
return dateutil.parser.parse(answer).date()
|
||||
except:
|
||||
raise ValidationError(_('Invalid date input.'))
|
||||
elif self.type == Question.TYPE_TIME:
|
||||
if isinstance(answer, time):
|
||||
return answer
|
||||
try:
|
||||
return dateutil.parser.parse(answer).time()
|
||||
except:
|
||||
raise ValidationError(_('Invalid time input.'))
|
||||
elif self.type == Question.TYPE_DATETIME and answer:
|
||||
if isinstance(answer, datetime):
|
||||
return answer
|
||||
try:
|
||||
dt = dateutil.parser.parse(answer)
|
||||
if is_naive(dt):
|
||||
dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
|
||||
return dt
|
||||
except:
|
||||
raise ValidationError(_('Invalid datetime input.'))
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
class QuestionOption(models.Model):
|
||||
question = models.ForeignKey('Question', related_name='options')
|
||||
answer = I18nCharField(verbose_name=_('Answer'))
|
||||
position = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.answer)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question option")
|
||||
verbose_name_plural = _("Question options")
|
||||
ordering = ('position', 'id')
|
||||
|
||||
|
||||
class Quota(LoggedModel):
|
||||
"""
|
||||
@@ -653,7 +806,7 @@ class Quota(LoggedModel):
|
||||
|
||||
Please read the documentation section on quotas carefully before doing
|
||||
anything with quotas. This might confuse you otherwise.
|
||||
http://docs.pretix.eu/en/latest/development/concepts.html#restriction-by-number
|
||||
https://docs.pretix.eu/en/latest/development/concepts.html#quotas
|
||||
|
||||
The AVAILABILITY_* constants represent various states of a quota allowing
|
||||
its items/variations to be up for sale.
|
||||
@@ -667,7 +820,7 @@ class Quota(LoggedModel):
|
||||
again if those people do not proceed to the checkout.
|
||||
|
||||
AVAILABILITY_ORDERED
|
||||
This item is currently not availalbe for sale because all available
|
||||
This item is currently not available for sale because all available
|
||||
items are ordered. It might become available again if those people
|
||||
do not pay.
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from pretix.base.signals import logentry_object_link
|
||||
|
||||
|
||||
class VisibleOnlyManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(visible=True)
|
||||
|
||||
|
||||
class LogEntry(models.Model):
|
||||
"""
|
||||
Represents a change or action that has been performed on another object
|
||||
@@ -36,9 +41,13 @@ 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)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
action_type = models.CharField(max_length=255)
|
||||
data = models.TextField(default='{}')
|
||||
visible = models.BooleanField(default=True)
|
||||
|
||||
objects = VisibleOnlyManager()
|
||||
all = models.Manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('-datetime',)
|
||||
|
||||
36
src/pretix/base/models/notifications.py
Normal file
36
src/pretix/base/models/notifications.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class NotificationSetting(models.Model):
|
||||
"""
|
||||
Stores that a user wants to get notifications of a certain type via a certain
|
||||
method for a certain event. If event is None, the notification shall be sent
|
||||
for all events the user has access to.
|
||||
|
||||
:param user: The user to nofify.
|
||||
:type user: User
|
||||
:param action_type: The type of action to notify for.
|
||||
:type action_type: str
|
||||
:param event: The event to notify for.
|
||||
:type event: Event
|
||||
:param method: The method to notify with.
|
||||
:type method: str
|
||||
:param enabled: Indicates whether the specified notification is enabled. If no
|
||||
event is set, this must always be true. If no event is set, setting
|
||||
this to false is equivalent to deleting the object.
|
||||
:type enabled: bool
|
||||
"""
|
||||
CHANNELS = (
|
||||
('mail', _('E-mail')),
|
||||
)
|
||||
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
||||
related_name='notification_settings')
|
||||
action_type = models.CharField(max_length=255)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE,
|
||||
related_name='notification_settings')
|
||||
method = models.CharField(max_length=255, choices=CHANNELS)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'action_type', 'event', 'method')
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -15,6 +16,7 @@ from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
@@ -160,6 +162,13 @@ class Order(LoggedModel):
|
||||
help_text=_("The text entered in this field will not be visible to the user and is available for your "
|
||||
"convenience.")
|
||||
)
|
||||
checkin_attention = models.BooleanField(
|
||||
verbose_name=_('Requires special attention'),
|
||||
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.')
|
||||
)
|
||||
expiry_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
@@ -188,7 +197,7 @@ class Order(LoggedModel):
|
||||
def full_code(self):
|
||||
"""
|
||||
An order code which is unique among all events of a single organizer,
|
||||
built by contatenating the event slug and the order code.
|
||||
built by concatenating the event slug and the order code.
|
||||
"""
|
||||
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
|
||||
|
||||
@@ -211,6 +220,12 @@ class Order(LoggedModel):
|
||||
def net_total(self):
|
||||
return self.total - self.tax_total
|
||||
|
||||
def cancel_allowed(self):
|
||||
return (
|
||||
self.status == Order.STATUS_PENDING
|
||||
or (self.status == Order.STATUS_PAID and self.total == Decimal('0.00'))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def normalize_code(code):
|
||||
tr = str.maketrans({
|
||||
@@ -270,7 +285,7 @@ class Order(LoggedModel):
|
||||
"""
|
||||
positions = self.positions.all().select_related('item')
|
||||
cancelable = all([op.item.allow_cancel for op in positions])
|
||||
return self.event.settings.cancel_allow_user and cancelable
|
||||
return self.cancel_allowed() and self.event.settings.cancel_allow_user and cancelable
|
||||
|
||||
@property
|
||||
def is_expired_by_time(self):
|
||||
@@ -387,6 +402,9 @@ class Order(LoggedModel):
|
||||
"""
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
|
||||
if not self.email:
|
||||
return
|
||||
|
||||
with language(self.locale):
|
||||
recipient = self.email
|
||||
try:
|
||||
@@ -498,6 +516,27 @@ class QuestionAnswer(models.Model):
|
||||
return str(_("No"))
|
||||
elif self.question.type == Question.TYPE_FILE:
|
||||
return str(_("<file>"))
|
||||
elif self.question.type == Question.TYPE_DATETIME and self.answer:
|
||||
try:
|
||||
d = dateutil.parser.parse(self.answer)
|
||||
if self.orderposition:
|
||||
tz = pytz.timezone(self.orderposition.order.event.settings.timezone)
|
||||
d = d.astimezone(tz)
|
||||
return date_format(d, "SHORT_DATETIME_FORMAT")
|
||||
except ValueError:
|
||||
return self.answer
|
||||
elif self.question.type == Question.TYPE_DATE and self.answer:
|
||||
try:
|
||||
d = dateutil.parser.parse(self.answer)
|
||||
return date_format(d, "SHORT_DATE_FORMAT")
|
||||
except ValueError:
|
||||
return self.answer
|
||||
elif self.question.type == Question.TYPE_TIME and self.answer:
|
||||
try:
|
||||
d = dateutil.parser.parse(self.answer)
|
||||
return date_format(d, "TIME_FORMAT")
|
||||
except ValueError:
|
||||
return self.answer
|
||||
else:
|
||||
return self.answer
|
||||
|
||||
@@ -519,7 +558,7 @@ class AbstractPosition(models.Model):
|
||||
:type variation: ItemVariation
|
||||
:param datetime: The datetime this item was put into the cart
|
||||
:type datetime: datetime
|
||||
:param expires: The date until this item is guarenteed to be reserved
|
||||
:param expires: The date until this item is guaranteed to be reserved
|
||||
:type expires: datetime
|
||||
:param price: The price of this item
|
||||
:type price: decimal.Decimal
|
||||
@@ -585,7 +624,7 @@ class AbstractPosition(models.Model):
|
||||
else:
|
||||
return {}
|
||||
|
||||
def cache_answers(self):
|
||||
def cache_answers(self, all=True):
|
||||
"""
|
||||
Creates two properties on the object.
|
||||
(1) answ: a dictionary of question.id → answer string
|
||||
@@ -598,7 +637,13 @@ class AbstractPosition(models.Model):
|
||||
# We need to clone our question objects, otherwise we will override the cached
|
||||
# answers of other items in the same cart if the question objects have been
|
||||
# selected via prefetch_related
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
if not all:
|
||||
if hasattr(self.item, 'questions_to_ask'):
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
else:
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False))
|
||||
else:
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
for q in self.questions:
|
||||
if q.id in self.answ:
|
||||
q.answer = self.answ[q.id]
|
||||
@@ -623,10 +668,12 @@ class OrderFee(models.Model):
|
||||
"""
|
||||
FEE_TYPE_PAYMENT = "payment"
|
||||
FEE_TYPE_SHIPPING = "shipping"
|
||||
FEE_TYPE_SERVICE = "service"
|
||||
FEE_TYPE_OTHER = "other"
|
||||
FEE_TYPES = (
|
||||
(FEE_TYPE_PAYMENT, _("Payment fee")),
|
||||
(FEE_TYPE_SHIPPING, _("Shipping fee")),
|
||||
(FEE_TYPE_SERVICE, _("Service fee")),
|
||||
(FEE_TYPE_OTHER, _("Other fees")),
|
||||
)
|
||||
|
||||
@@ -769,6 +816,9 @@ class OrderPosition(AbstractPosition):
|
||||
'order_code': order.code
|
||||
})
|
||||
|
||||
# Delete afterwards. Deleting in between might cause deletion of things related to add-ons
|
||||
# due to the deletion cascade.
|
||||
for cartpos in cp:
|
||||
cartpos.delete()
|
||||
return ops
|
||||
|
||||
@@ -821,7 +871,7 @@ class CartPosition(AbstractPosition):
|
||||
the checkout process. This has all properties of AbstractPosition.
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Evnt
|
||||
:type event: Event
|
||||
:param cart_id: The user session that contains this cart position
|
||||
:type cart_id: str
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
@@ -8,6 +9,7 @@ from i18nfield.fields import I18nCharField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
@@ -23,6 +25,13 @@ class TaxedPrice:
|
||||
def __repr__(self):
|
||||
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
||||
|
||||
def print(self, currency):
|
||||
return '{} + {}% = {}'.format(
|
||||
money_filter(self.net, currency),
|
||||
localize(self.rate),
|
||||
money_filter(self.gross, currency)
|
||||
)
|
||||
|
||||
|
||||
TAXED_ZERO = TaxedPrice(
|
||||
gross=Decimal('0.00'),
|
||||
@@ -80,6 +89,7 @@ class TaxRule(LoggedModel):
|
||||
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
|
||||
'if configured above.'),
|
||||
)
|
||||
custom_rules = models.TextField(blank=True, null=True)
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderFee, OrderPosition
|
||||
@@ -129,10 +139,12 @@ class TaxRule(LoggedModel):
|
||||
|
||||
if base_price_is == 'gross':
|
||||
gross = base_price
|
||||
net = gross - round_decimal(base_price * (1 - 100 / (100 + self.rate)))
|
||||
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
|
||||
self.event.currency if self.event else None)
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal(net * (1 + self.rate / 100))
|
||||
gross = round_decimal((net * (1 + self.rate / 100)),
|
||||
self.event.currency if self.event else None)
|
||||
else:
|
||||
raise ValueError('Unknown base price type: {}'.format(base_price_is))
|
||||
|
||||
@@ -141,7 +153,27 @@ class TaxRule(LoggedModel):
|
||||
rate=self.rate, name=self.name
|
||||
)
|
||||
|
||||
def get_matching_rule(self, invoice_address):
|
||||
rules = json.loads(self.custom_rules)
|
||||
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):
|
||||
if self.custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule['action'] == 'reverse'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
return False
|
||||
|
||||
@@ -160,6 +192,10 @@ class TaxRule(LoggedModel):
|
||||
return False
|
||||
|
||||
def tax_applicable(self, invoice_address):
|
||||
if self.custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule.get('action', 'vat') == 'vat'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
# No reverse charge rules? Always apply VAT!
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -42,7 +42,7 @@ class Voucher(LoggedModel):
|
||||
:param max_usages: The number of times this voucher can be redeemed
|
||||
:type max_usages: int
|
||||
:param redeemed: The number of times this voucher already has been redeemed
|
||||
:type redeemed: bool
|
||||
:type redeemed: int
|
||||
:param valid_until: The expiration date of this voucher (optional)
|
||||
:type valid_until: datetime
|
||||
:param block_quota: If set to true, this voucher will reserve quota for its holder
|
||||
@@ -368,9 +368,15 @@ class Voucher(LoggedModel):
|
||||
"""
|
||||
if self.value is not None:
|
||||
if self.price_mode == 'set':
|
||||
return self.value
|
||||
p = self.value
|
||||
elif self.price_mode == 'subtract':
|
||||
return max(original_price - self.value, Decimal('0.00'))
|
||||
p = max(original_price - self.value, Decimal('0.00'))
|
||||
elif self.price_mode == 'percent':
|
||||
return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
|
||||
p = round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
|
||||
else:
|
||||
p = original_price
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
if places < 2:
|
||||
return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
|
||||
return p
|
||||
return original_price
|
||||
|
||||
@@ -87,7 +87,7 @@ class WaitingListEntry(LoggedModel):
|
||||
if self.variation
|
||||
else self.item.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
|
||||
)
|
||||
if availability[1] < 1:
|
||||
if availability[1] is None or availability[1] < 1:
|
||||
raise WaitingListException(_('This product is currently not available.'))
|
||||
if self.voucher:
|
||||
raise WaitingListException(_('A voucher has already been sent to this person.'))
|
||||
|
||||
234
src/pretix/base/notifications.py
Normal file
234
src/pretix/base/notifications.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import logging
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.signals import register_notification_types
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_TYPES = None
|
||||
|
||||
|
||||
NotificationAttribute = namedtuple('NotificationAttribute', ('title', 'value'))
|
||||
NotificationAction = namedtuple('NotificationAction', ('label', 'url'))
|
||||
|
||||
|
||||
class Notification:
|
||||
"""
|
||||
Represents a notification that is sent/shown to a user. A notification consists of:
|
||||
|
||||
* one ``event`` reference
|
||||
* one ``title`` text that is shown e.g. in the email subject or in a headline
|
||||
* optionally one ``detail`` text that may or may not be shown depending on the notification method
|
||||
* optionally one ``url`` that should be absolute and point to the context of an notification (e.g. an order)
|
||||
* optionally a number of attributes consisting of a title and a value that can be used to add additional details
|
||||
to the notification (e.g. "Customer: ABC")
|
||||
* optionally a number of actions that may or may not be shown as buttons depending on the notification method,
|
||||
each consisting of a button label and an absolute URL to point to.
|
||||
"""
|
||||
|
||||
def __init__(self, event: Event, title: str, detail: str=None, url: str=None):
|
||||
self.title = title
|
||||
self.event = event
|
||||
self.detail = detail
|
||||
self.url = url
|
||||
self.attributes = []
|
||||
self.actions = []
|
||||
|
||||
def add_action(self, label, url):
|
||||
"""
|
||||
Add an action to the notification, defined by a label and an url. An example could be a label of "View order"
|
||||
and an url linking to the order detail page.
|
||||
"""
|
||||
self.actions.append(NotificationAction(label, url))
|
||||
|
||||
def add_attribute(self, title, value):
|
||||
"""
|
||||
Add an attribute to the notification, defined by a title and a value. An example could be a title of
|
||||
"Date" and a value of "2017-12-14".
|
||||
"""
|
||||
self.attributes.append(NotificationAttribute(title, value))
|
||||
|
||||
|
||||
class NotificationType:
|
||||
def __init__(self, event: Event = None):
|
||||
self.event = event
|
||||
|
||||
def __repr__(self):
|
||||
return '<NotificationType: {}>'.format(self.action_type)
|
||||
|
||||
@property
|
||||
def action_type(self) -> str:
|
||||
"""
|
||||
The action_type string that this notification handles, for example
|
||||
``"pretix.event.order.paid"``. Only one notification type should be registered
|
||||
per action type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name of this notification type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def required_permission(self) -> str:
|
||||
"""
|
||||
The permission a user needs to hold for the related event to receive this
|
||||
notification.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def build_notification(self, logentry: LogEntry) -> Notification:
|
||||
"""
|
||||
This is the main function that you should override. It is supposed to turn a log entry
|
||||
object into a notification object that can then be rendered e.g. into an email.
|
||||
"""
|
||||
return Notification(
|
||||
logentry.event,
|
||||
logentry.display()
|
||||
)
|
||||
|
||||
|
||||
def get_all_notification_types(event=None):
|
||||
global _ALL_TYPES
|
||||
|
||||
if event is None and _ALL_TYPES:
|
||||
return _ALL_TYPES
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_notification_types.send(event):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.action_type] = r
|
||||
else:
|
||||
types[ret.action_type] = ret
|
||||
if event is None:
|
||||
_ALL_TYPES = types
|
||||
return types
|
||||
|
||||
|
||||
class ActionRequiredNotificationType(NotificationType):
|
||||
required_permission = "can_change_orders"
|
||||
action_type = "pretix.event.action_required"
|
||||
verbose_name = _("Administrative action required")
|
||||
|
||||
def build_notification(self, logentry: LogEntry):
|
||||
control_url = build_absolute_uri(
|
||||
'control:event.requiredactions',
|
||||
kwargs={
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
}
|
||||
)
|
||||
|
||||
n = Notification(
|
||||
event=logentry.event,
|
||||
title=_('Administrative action required'),
|
||||
detail=_('Something happened in your event that our system cannot handle automatically, e.g. an external '
|
||||
'refund. You need to resolve it manually or choose to ignore it, depending on the issue at hand.'),
|
||||
url=control_url
|
||||
)
|
||||
n.add_action(_('View all unresolved problems'), control_url)
|
||||
return n
|
||||
|
||||
|
||||
class ParametrizedOrderNotificationType(NotificationType):
|
||||
required_permission = "can_view_orders"
|
||||
|
||||
def __init__(self, event, action_type, verbose_name, title):
|
||||
self._action_type = action_type
|
||||
self._verbose_name = verbose_name
|
||||
self._title = title
|
||||
super().__init__(event)
|
||||
|
||||
@property
|
||||
def action_type(self):
|
||||
return self._action_type
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return self._verbose_name
|
||||
|
||||
def build_notification(self, logentry: LogEntry):
|
||||
order = logentry.content_object
|
||||
|
||||
order_url = build_absolute_uri(
|
||||
'control:event.order',
|
||||
kwargs={
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
'code': order.code
|
||||
}
|
||||
)
|
||||
|
||||
n = Notification(
|
||||
event=logentry.event,
|
||||
title=self._title.format(order=order, event=logentry.event),
|
||||
url=order_url
|
||||
)
|
||||
n.add_attribute(_('Order code'), order.code)
|
||||
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
|
||||
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
|
||||
n.add_attribute(_('Order status'), order.get_status_display())
|
||||
n.add_attribute(_('Order positions'), str(order.positions.count()))
|
||||
n.add_action(_('View order details'), order_url)
|
||||
return n
|
||||
|
||||
|
||||
@receiver(register_notification_types, dispatch_uid="base_register_default_notification_types")
|
||||
def register_default_notification_types(sender, **kwargs):
|
||||
return (
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.placed',
|
||||
_('New order placed'),
|
||||
_('A new order has been placed: {order.code}'),
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.paid',
|
||||
_('Order marked as paid'),
|
||||
_('Order {order.code} has been marked as paid.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.canceled',
|
||||
_('Order canceled'),
|
||||
_('Order {order.code} has been canceled.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.modified',
|
||||
_('Order information changed'),
|
||||
_('The ticket information of order {order.code} has been changed.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.contact.changed',
|
||||
_('Order contact address changed'),
|
||||
_('The contact address of order {order.code} has been changed.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.changed',
|
||||
_('Order changed'),
|
||||
_('Order {order.code} has been changed.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.refunded',
|
||||
_('Order refunded'),
|
||||
_('Order {order.code} has been refunded.')
|
||||
),
|
||||
ActionRequiredNotificationType(
|
||||
sender,
|
||||
)
|
||||
)
|
||||
@@ -1,25 +1,26 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
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 ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
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.helpers.money import DecimalTextInput
|
||||
from pretix.presale.views import get_cart_total
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
@@ -50,6 +51,16 @@ class BasePaymentProvider:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
@property
|
||||
def is_implicit(self) -> bool:
|
||||
"""
|
||||
Returns whether or whether not this payment provider is an "implicit" payment provider that will
|
||||
*always* and unconditionally be used if is_allowed() returns True and does not require any input.
|
||||
This is intended to be used by the FreePaymentProvider, which skips the payment choice page.
|
||||
By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_meta(self) -> bool:
|
||||
"""
|
||||
@@ -81,10 +92,15 @@ class BasePaymentProvider:
|
||||
fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
|
||||
fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
|
||||
fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True)
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
if fee_reverse_calc:
|
||||
return round_decimal((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price)
|
||||
return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
else:
|
||||
return round_decimal(price * fee_percent / 100) + fee_abs
|
||||
return (price * fee_percent / 100 + fee_abs).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
@@ -146,6 +162,7 @@ class BasePaymentProvider:
|
||||
.. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
|
||||
implementation.
|
||||
"""
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return OrderedDict([
|
||||
('_enabled',
|
||||
forms.BooleanField(
|
||||
@@ -156,7 +173,10 @@ class BasePaymentProvider:
|
||||
forms.DecimalField(
|
||||
label=_('Additional fee'),
|
||||
help_text=_('Absolute value'),
|
||||
required=False
|
||||
localize=True,
|
||||
required=False,
|
||||
decimal_places=places,
|
||||
widget=DecimalTextInput(places=places)
|
||||
)),
|
||||
('_fee_percent',
|
||||
forms.DecimalField(
|
||||
@@ -164,7 +184,8 @@ class BasePaymentProvider:
|
||||
help_text=_('Percentage of the order total. Note that this percentage will currently only '
|
||||
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
|
||||
'fees, if there are any.'),
|
||||
required=False
|
||||
localize=True,
|
||||
required=False,
|
||||
)),
|
||||
('_availability_date',
|
||||
RelativeDateField(
|
||||
@@ -184,7 +205,9 @@ class BasePaymentProvider:
|
||||
('_invoice_text',
|
||||
I18nFormField(
|
||||
label=_('Text on invoices'),
|
||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices.'),
|
||||
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
|
||||
'This will only be used if the invoice is generated before the order is paid. If the '
|
||||
'invoice is generated later, it will show a text stating that it has already been paid.'),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}}
|
||||
@@ -205,6 +228,8 @@ class BasePaymentProvider:
|
||||
The default implementation returns the content of the _invoice_text configuration
|
||||
variable (an I18nString), or an empty string if unconfigured.
|
||||
"""
|
||||
if order.status == Order.STATUS_PAID:
|
||||
return pgettext_lazy('invoice', 'The payment for this invoice has already been received.')
|
||||
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
|
||||
|
||||
@property
|
||||
@@ -324,7 +349,7 @@ class BasePaymentProvider:
|
||||
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 behaviour
|
||||
if the input was valid and the frontend should continue with default behavior
|
||||
or a string containing a URL if the user should be redirected somewhere else.
|
||||
|
||||
On errors, you should use Django's message framework to display an error message
|
||||
@@ -375,7 +400,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
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.
|
||||
If you need any special behaviour, 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.
|
||||
|
||||
@@ -523,8 +548,8 @@ class BasePaymentProvider:
|
||||
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 behaviour or None to continue
|
||||
with default behaviour.
|
||||
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.
|
||||
@@ -548,6 +573,10 @@ class PaymentException(Exception):
|
||||
|
||||
class FreeOrderProvider(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def is_implicit(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return True
|
||||
@@ -588,8 +617,8 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
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 behaviour or None to continue
|
||||
with default behaviour.
|
||||
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.
|
||||
|
||||
@@ -112,7 +112,7 @@ class CartManager:
|
||||
def _check_presale_dates(self):
|
||||
if self.event.presale_start and self.now_dt < self.event.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
if self.event.presale_end and self.now_dt > self.event.presale_end:
|
||||
if self.event.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
@@ -188,7 +188,7 @@ class CartManager:
|
||||
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
|
||||
if op.subevent and op.subevent.presale_end and self.now_dt > op.subevent.presale_end:
|
||||
if op.subevent and op.subevent.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
@@ -667,7 +667,8 @@ def get_fees(event, request, total, invoice_address, provider):
|
||||
tax_rule=payment_fee_tax_rule
|
||||
))
|
||||
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address):
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
|
||||
total=total):
|
||||
fees += resp
|
||||
|
||||
return fees
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user