Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
3c86fc697b Bump version to 3.6.0.post1 2020-02-17 21:02:09 +01:00
Raphael Michel
efb7a48200 Fix #1582 - Add explicit requirement to packaging library 2020-02-17 21:01:14 +01:00
573 changed files with 94362 additions and 168914 deletions

View File

@@ -1,42 +0,0 @@
name: Documentation
on:
push:
branches: [ master ]
paths-ignore:
- 'src/pretix/locale/**'
- 'src/pretix/static/**'
- 'src/tests/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'src/pretix/locale/**'
- 'src/pretix/static/**'
- 'src/tests/**'
jobs:
spelling:
name: Spellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell aspell-en
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
- name: Spellcheck docs
run: make spelling
working-directory: ./doc
- name:
run: '[ ! -s _build/spelling/output.txt ]'
working-directory: ./doc

View File

@@ -1,62 +0,0 @@
name: Strings
on:
push:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:
compile:
runs-on: ubuntu-latest
name: Check gettext syntax
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
- name: Compile jsi18n
run: python manage.py compilejsi18n
working-directory: ./src
spelling:
runs-on: ubuntu-latest
name: Spellcheck
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -1,55 +0,0 @@
name: Code Style
on:
push:
branches: [ master ]
paths-ignore:
- 'src/pretix/locale/**'
- 'src/pretix/static/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'src/pretix/locale/**'
- 'src/pretix/static/**'
jobs:
isort:
name: isort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Run isort
run: isort -c .
working-directory: ./src
flake:
name: flake8
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -1,75 +0,0 @@
name: Tests
on:
push:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:
test:
runs-on: ubuntu-latest
name: Tests
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
database: [sqlite, postgres, mysql]
exclude:
- database: mysql
python-version: 3.7
- database: sqlite
python-version: 3.7
- database: mysql
python-version: 3.6
- database: sqlite
python-version: 3.6
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.4'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
if: matrix.database == 'postgres'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mysql-client
- name: Install Python dependencies
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
- name: Run checks
run: python manage.py check
working-directory: ./src
- name: Compile
working-directory: ./src
run: make all compress
- name: Run tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
file: src/coverage.xml
fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.8'

View File

@@ -5,11 +5,7 @@ tests:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
- cd src
- python manage.py check
- make all compress
- py.test --reruns 3 -n 3 tests
- XDG_CACHE_HOME=/cache bash .travis.sh tests
tags:
- python3
except:
@@ -20,17 +16,15 @@ pypi:
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- check-manifest
- python setup.py sdist bdist_wheel
- twine check dist/*
- twine upload dist/*
- python setup.py sdist upload
- python setup.py bdist_wheel upload
tags:
- python3
only:

68
.travis.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
set -e
set -x
echo "Executing job $1"
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_mysql.cfg" ]; then
mysql -u root -e 'CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;'
pip3 install -Ur src/requirements/mysql.txt
fi
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
psql -c 'create database travis_ci_test;' -U postgres
fi
if [ "$1" == "style" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements.txt -r src/requirements/dev.txt
cd src
flake8 .
isort -c -rc -df .
fi
if [ "$1" == "doctests" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
cd doc
make doctest
fi
if [ "$1" == "doc-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
cd doc
make spelling
if [ -s _build/spelling/output.txt ]; then
exit 1
fi
fi
if [ "$1" == "translation-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
cd src
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 3 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
coverage run -m py.test --reruns 5 tests && codecov
fi
if [ "$1" == "plugins" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
cd src
python setup.py develop
make all compress
pushd ~
git clone --depth 1 https://github.com/pretix/pretix-cartshare.git
cd pretix-cartshare
python setup.py develop
make
py.test --reruns 5 tests
popd
fi

45
.travis.yml Normal file
View File

@@ -0,0 +1,45 @@
language: python
dist: xenial
sudo: false
install:
- pip install -U pip wheel setuptools
script:
- bash .travis.sh $JOB
cache:
directories:
- $HOME/.cache/pip
services:
- mysql
- postgresql
matrix:
include:
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.7
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7
env: JOB=style
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7
env: JOB=doc-spelling
- python: 3.7
env: JOB=translation-spelling
addons:
postgresql: "9.4"
mariadb: '10.3'
apt:
packages:
- enchant
- myspell-de-de
- aspell-en
- sqlite3
sources:
- travis-ci/sqlite3
branches:
except:
- /^weblate-.*/

View File

@@ -29,7 +29,7 @@ RUN apt-get update && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static
ENV LC_ALL=C.UTF-8 \

View File

@@ -4,10 +4,11 @@ pretix
.. image:: https://img.shields.io/pypi/v/pretix.svg
:target: https://pypi.python.org/pypi/pretix
.. image:: https://github.com/pretix/pretix/workflows/Documentation/badge.svg
.. image:: https://readthedocs.org/projects/pretix/badge/?version=latest
:target: https://docs.pretix.eu/en/latest/
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
.. image:: https://travis-ci.org/pretix/pretix.svg?branch=master
:target: https://travis-ci.org/pretix/pretix
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
:target: https://codecov.io/gh/pretix/pretix
@@ -19,8 +20,9 @@ Reinventing ticket presales, one ticket at a time.
Project status & release cycle
------------------------------
While there is always a lot to do and improve on, pretix by now has been in use for thousands of events
conferences that sold millions of tickets combined. We therefore think of pretix as being stable and ready to use.
While there is always a lot to do and improve on, pretix by now has been in use for more than a dozen
conferences that sold over ten thousand tickets combined without major problems. We therefore think of
pretix as being stable and ready to use.
If you want to use or extend pretix, we strongly recommend to follow our `blog`_. We will announce all
releases there. You can always find the latest stable version on PyPI or in the ``release/X.Y`` branch of
@@ -29,13 +31,9 @@ the sense that it does not break your data, but its APIs might change without p
To get started using pretix on your own server, look at the `installation guide`_ in our documentation.
Support
-------
This project is 100 percent free and open source software. You are welcome to ask questions in the GitHub
repository. Private support via email or phone is only offered to customers of our pretix Hosted or pretix
Enterprise offerings. If you are interested in commercial support, hosting services or supporting this project
financially, please go to `pretix.eu`_ or contact us at support@pretix.eu.
This project is 100 percent free and open source software. If you are interested in commercial support,
hosting services or supporting this project financially, please go to `pretix.eu`_ or contact us at
support@pretix.eu.
Contributing
------------
@@ -55,8 +53,8 @@ License
The code in this repository is published under the terms of the Apache License.
See the LICENSE file for the complete license text.
This project is maintained by Raphael Michel. See the AUTHORS file for a list of all
the awesome folks who contributed to this project.
This project is maintained by Raphael Michel <mail@raphaelmichel.de>. See the
AUTHORS file for a list of all the awesome folks who contributed to this project.
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html

View File

@@ -19,7 +19,7 @@ fi
python3 -m pretix migrate --noinput
if [ "$1" == "all" ]; then
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.conf
exec sudo /usr/bin/supervisord -n -c /etc/supervisord.conf
fi
if [ "$1" == "webworker" ]; then

View File

@@ -92,11 +92,9 @@ Example::
``trust_x_forwarded_proto``
Specifies whether the ``X-Forwarded-Proto`` header can be trusted. Only set to ``on`` if you have a reverse
proxy that actively removes and re-adds the header to make sure the correct value is set.
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
Defaults to ``off``.
``csp_log``
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
Locale settings
---------------
@@ -339,15 +337,6 @@ application. If you want to use sentry, you need to set a DSN in the configurati
You will be given this value by your sentry installation.
Caching
-------
You can adjust some caching settings to control how much storage pretix uses::
[cache]
tickets=48 ; Number of hours tickets (PDF, passbook, …) are cached
Secret length
-------------

View File

@@ -12,4 +12,3 @@ This documentation is for everyone who wants to install pretix on a server.
config
maintainance
scaling
indexes

View File

@@ -1,73 +0,0 @@
Additional database indices
===========================
If you have a large pretix database, some features such as search for orders or events might turn pretty slow.
For PostgreSQL, we have compiled a list of additional database indexes that you can add to speed things up.
Just like any index, they in turn make write operations insignificantly slower and cause the database to use
more disk space.
The indexes aren't automatically created by pretix since Django does not allow us to do so only on PostgreSQL
(and they won't work on other databases). Also, they're really not necessary if you're not having tens of
thousands of records in your database.
However, this also means they won't automatically adapt if some of the referred fields change in future updates of pretix
and you might need to re-check this page and change them manually.
Here is the currently recommended set of commands::
CREATE EXTENSION pg_trgm;
CREATE INDEX CONCURRENTLY pretix_addidx_event_slug
ON pretixbase_event
USING gin (upper("slug") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_event_name
ON pretixbase_event
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_code
ON pretixbase_order
USING gin (upper("code") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code
ON pretixbase_voucher
USING gin (upper("code") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu1
ON "pretixbase_invoice" (UPPER("invoice_no"));
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu2
ON "pretixbase_invoice" (UPPER("full_invoice_no"));
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_name
ON pretixbase_organizer
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_slug
ON pretixbase_organizer
USING gin (upper("slug") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_email
ON pretixbase_order
USING gin (upper("email") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_comment
ON pretixbase_order
USING gin (upper("comment") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
ON pretixbase_orderposition
USING gin (upper("attendee_name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
ON pretixbase_orderposition
USING gin (upper("secret") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email
ON pretixbase_orderposition
USING gin (upper("attendee_email") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_ia_name
ON pretixbase_invoiceaddress
USING gin (upper("name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
ON pretixbase_invoiceaddress
USING gin (upper("company") gin_trgm_ops);
Also, if you use our ``pretix-shipping`` plugin::
CREATE INDEX CONCURRENTLY pretix_addidx_sa_name
ON pretix_shipping_shippingaddress
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_sa_company
ON pretix_shipping_shippingaddress
USING gin (upper("company") gin_trgm_ops);

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
--sysctl net.core.somaxconn=4096 \
--sysctl net.core.somaxconn=4096
pretix/standalone:stable all
ExecStop=/usr/bin/docker stop %n
@@ -290,7 +290,7 @@ to re-build your custom image after you pulled ``pretix/standalone`` if you want
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _redis website: https://redis.io/topics/security

View File

@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones.
Requirements
@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -133,7 +133,7 @@ command if you're running MySQL::
(venv)$ pip3 install "pretix[postgres]" gunicorn
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::
@@ -308,7 +308,7 @@ example::
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -92,8 +92,7 @@ pretix_task_duration_seconds
pretix_model_instances
Gauge. Measures number of instances of a certain model within the database, labeled with
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
most tables when running on PostgreSQL to mitigate performance impact.
the ``model`` name.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/

View File

@@ -7,6 +7,9 @@ This part of the documentation contains information about the REST-style API
exposed by pretix since version 1.5 that can be used by third-party programs
to interact with pretix and its data structures.
Currently, the API provides mostly read-only capabilities, but it will be extended
in functionality over time.
.. toctree::
:maxdepth: 2

View File

@@ -30,9 +30,6 @@ position_count integer Number of ticke
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.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -51,15 +48,6 @@ rules object Custom check-in
The ``auto_checkin_sales_channels`` field has been added.
.. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
``allow_entry_after_exit``, and ``rules`` attributes have been added.
.. versionchanged:: 3.11
The ``subevent_match`` and ``exclude`` query parameters have been added.
Endpoints
---------
@@ -101,9 +89,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -113,8 +98,6 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query integer subevent: Only return check-in lists of the sub-event with the given ID
:query integer subevent_match: Only return check-in lists that are valid for the sub-event with the given ID (i.e. also lists valid for all subevents)
:query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -150,9 +133,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -249,8 +229,6 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -273,8 +251,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -327,8 +303,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -608,7 +582,6 @@ Order position endpoints
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
@@ -723,7 +696,6 @@ Order position endpoints
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
* ``already_redeemed`` - Ticket already has been redeemed
* ``product`` - Tickets with this product may not be scanned at this device
* ``rules`` - Check-in prevented by a user-defined rule
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -43,7 +43,6 @@ seating_plan integer If reserved sea
seat_category_mapping object An object mapping categories of the seating plan
(strings) to items in the event (integers or ``null``).
timezone string Event timezone name
item_meta_properties object Item-specific meta data parameters and default values.
===================================== ========================== =======================================================
@@ -80,10 +79,6 @@ item_meta_properties object Item-specific m
The attribute ``timezone`` has been added.
.. versionchanged:: 3.7
The attribute ``item_meta_properties`` has been added.
Endpoints
---------
@@ -138,7 +133,6 @@ Endpoints
"seating_plan": null,
"seat_category_mapping": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.banktransfer"
"pretix.plugins.stripe"
@@ -210,7 +204,6 @@ Endpoints
"seat_category_mapping": {},
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.banktransfer"
"pretix.plugins.stripe"
@@ -263,7 +256,6 @@ Endpoints
"has_subevents": false,
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.stripe",
"pretix.plugins.paypal"
@@ -298,7 +290,6 @@ Endpoints
"has_subevents": false,
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.stripe",
"pretix.plugins.paypal"
@@ -353,7 +344,6 @@ Endpoints
"has_subevents": false,
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.stripe",
"pretix.plugins.paypal"
@@ -388,7 +378,6 @@ Endpoints
"seat_category_mapping": {},
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.stripe",
"pretix.plugins.paypal"
@@ -455,7 +444,6 @@ Endpoints
"seat_category_mapping": {},
"meta_data": {},
"timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [
"pretix.plugins.banktransfer",
"pretix.plugins.stripe",
@@ -526,7 +514,7 @@ information about the properties.
Get current values of event settings.
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
Permission required: "Can change event settings"
**Example request**:

View File

@@ -18,8 +18,6 @@ secret string Gift card code
value money (string) Current gift card value
currency string Currency of the value (can not be modified later)
testmode boolean Whether this is a test gift card
expires datetime Expiry date (or ``null``)
conditions string Special terms and conditions for this card (or ``null``)
===================================== ========================== =======================================================
Endpoints
@@ -55,17 +53,12 @@ Endpoints
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"expires": null,
"conditions": null,
"value": "13.37"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string secret: Only show gift cards with the given secret.
:query boolean testmode: Filter for gift cards that are (not) in test mode.
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -96,14 +89,11 @@ Endpoints
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"expires": null,
"conditions": null,
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the gift card to fetch
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
@@ -140,8 +130,6 @@ Endpoints
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"expires": null,
"conditions": null,
"value": "13.37"
}
@@ -188,8 +176,6 @@ Endpoints
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"expires": null,
"conditions": null,
"value": "14.00"
}
@@ -232,8 +218,6 @@ Endpoints
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"expires": null,
"conditions": null,
"value": "15.37"
}
@@ -243,7 +227,6 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure

View File

@@ -24,7 +24,6 @@ addon_category integer Internal ID of
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
multi_allowed boolean Adding the same item multiple times is allowed
price_included boolean Adding this add-on to the item is free
===================================== ========================== =======================================================
@@ -66,7 +65,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 0,
"multi_allowed": false,
"price_included": true
},
{
@@ -75,7 +73,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
]
@@ -115,7 +112,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -145,7 +141,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -163,7 +158,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -212,7 +206,6 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}

View File

@@ -104,7 +104,6 @@ addons list of objects Definition of a
├ 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
├ multi_allowed boolean Adding the same item multiple times is allowed
└ price_included boolean Adding this add-on to the item is free
bundles list of objects Definition of bundles that are included in this item.
Only writable during creation,
@@ -115,7 +114,6 @@ bundles list of objects Definition of b
└ designated_price money (string) Designated price of the bundled product. This will be
used to split the price of the base item e.g. for mixed
taxation. This is not added to the price.
meta_data object Values set for event-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 2.7
@@ -156,14 +154,6 @@ meta_data object Values set for
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
.. versionchanged:: 3.7
The attribute ``meta_data`` has been added.
.. versionchanged:: 3.10
The attribute ``multi_allowed`` has been added to ``addons``.
Notes
-----
@@ -218,7 +208,6 @@ Endpoints
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
"picture": null,
"available_from": null,
@@ -314,7 +303,6 @@ Endpoints
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
"picture": null,
"available_from": null,
@@ -391,7 +379,6 @@ Endpoints
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
"picture": null,
"available_from": null,
@@ -455,7 +442,6 @@ Endpoints
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
"picture": null,
"available_from": null,
@@ -551,7 +537,6 @@ Endpoints
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
"picture": null,
"available_from": null,

View File

@@ -151,18 +151,6 @@ last_modified datetime Last modificati
The ``order.fees.canceled`` attribute has been added.
.. versionchanged:: 3.8
The ``reactivate`` operation has been added.
.. versionchanged:: 3.10
The ``search`` query parameter has been added.
.. versionchanged:: 3.11
The ``exclude`` and ``subevent_after`` query parameter has been added.
.. _order-position-resource:
@@ -185,13 +173,6 @@ price money (string) Price of this p
attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
attendee_email string Specified attendee email address for this position (or ``null``)
company string Attendee company name (or ``null``)
street string Attendee street (or ``null``)
zipcode string Attendee ZIP code (or ``null``)
city string Attendee city (or ``null``)
country string Attendee country code (or ``null``)
state string Attendee state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position
@@ -203,7 +184,6 @@ pseudonymization_id string A random ID, e.
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
├ type string Type of scan (defaults to ``entry``)
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
@@ -256,14 +236,6 @@ pdf_data object Data object req
The attribute ``canceled`` has been added.
.. versionchanged:: 3.8
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
.. versionchanged:: 3.9
The ``checkin.type`` attribute has been added.
.. _order-payment-resource:
Order payment resource
@@ -408,12 +380,6 @@ List of all orders
"full_name": "Peter",
},
"attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null,
"tax_rate": "0.00",
"tax_value": "0.00",
@@ -426,7 +392,6 @@ List of all orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -476,7 +441,6 @@ List of all orders
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.
@@ -489,8 +453,6 @@ List of all orders
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date.
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -574,12 +536,6 @@ Fetching individual orders
"full_name": "Peter",
},
"attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
@@ -592,7 +548,6 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -861,9 +816,9 @@ Creating orders
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
creation.
* ``email`` (optional)
* ``email``
* ``locale``
* ``sales_channel`` (optional)
* ``sales_channel``
* ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
@@ -896,21 +851,15 @@ Creating orders
* ``positionid`` (optional, see below)
* ``item``
* ``variation`` (optional)
* ``variation``
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_name`` **or** ``attendee_name_parts``
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email`` (optional)
* ``company`` (optional)
* ``street`` (optional)
* ``zipcode`` (optional)
* ``city`` (optional)
* ``country`` (optional)
* ``state`` (optional)
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
* ``subevent`` (optional)
* ``subevent``
* ``answers``
* ``question``
@@ -934,8 +883,7 @@ Creating orders
during order generation and is not respected automatically when the order changes later.)
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
whether these emails are enabled for certain sales channels. Defaults to
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
@@ -943,13 +891,6 @@ Creating orders
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
immediately after the position itself.
Starting with pretix 3.7, you can add ``"simulate": true`` to the body to do a "dry run" of your order. This will
validate your order and return you an order object with the resulting prices, but will not create an actual order.
You can use this for testing or to look up prices. In this case, some attributes are ignored, such as whether
to send an email or what payment provider will be used. Note that some returned fields will contain empty values
(e.g. all ``id`` fields of positions will be zero) and some will contain fake values (e.g. the order code will
always be ``PREVIEW``). pretix plugins will not be triggered, so some special behavior might be missing as well.
**Example request**:
.. sourcecode:: http
@@ -1109,42 +1050,6 @@ Order state operations
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
still available.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"code": "ABC12",
"status": "n",
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to modify
:statuscode 200: no error
:statuscode 400: The order cannot be reactivated
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
Marks a paid order as unpaid.
@@ -1490,7 +1395,6 @@ List of all order positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1596,7 +1500,6 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}

View File

@@ -26,8 +26,6 @@ close_when_sold_out boolean If ``true``, th
again.
closed boolean Whether the quota is currently closed (see above
field).
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
have been scanned at an exit.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -38,10 +36,6 @@ release_after_exit boolean Whether the quo
The attributes ``close_when_sold_out`` and ``closed`` have been added.
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.
Endpoints
---------
@@ -289,7 +283,6 @@ Endpoints
"total_size": 1000,
"pending_orders": 25,
"paid_orders": 423,
"exited_orders": 0,
"cart_positions": 7,
"blocking_vouchers": 126,
"waiting_list": 0

View File

@@ -39,12 +39,10 @@ geo_lon float Longitude of th
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
├ disabled boolean If ``true``, item should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
variation_price_overrides list of objects List of variations for which this sub-event overrides
the default price
├ variation integer The internal variation ID
├ disabled boolean If ``true``, variation should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
meta_data object Values set for organizer-specific meta data parameters.
seating_plan integer If reserved seating is in use, the ID of a seating
@@ -76,10 +74,6 @@ seat_category_mapping object An object mappi
The attributes ``geo_lat`` and ``geo_lon`` have been added.
.. versionchanged:: 3.10
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
Endpoints
---------
@@ -131,7 +125,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -189,7 +182,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -224,7 +216,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -280,7 +271,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -317,7 +307,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -350,7 +339,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -439,7 +427,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],

View File

@@ -70,9 +70,6 @@ and ``checkin_list``.
only include the minimum amount of data necessary for you to fetch the changed objects from our
:ref:`rest-api` in an authenticated way.
.. warning:: In very rare cases, you could receive the same webhook notification twice. We try to avoid it, but we
prefer it over missing a notification.
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require

View File

@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
from django.urls import resolve, reverse
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.control.signals import nav_event

View File

@@ -29,22 +29,6 @@ that we'll provide in this plugin::
from .exporter import MyExporter
return MyExporter
Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your
exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_multievent_data_exporters
@receiver(register_multievent_data_exporters, dispatch_uid="multieventexporter_myexporter")
def register_multievent_data_exporter(sender, **kwargs):
from .exporter import MyExporter
return MyExporter
If your exporter supports both event-level and multi-event level exports, you will need to listen for both
signals.
The exporter class
------------------

View File

@@ -20,7 +20,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""
@@ -33,11 +33,11 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
.. automodule:: pretix.presale.signals
:members: order_info, order_info_top, order_meta_from_request
:members: order_info, order_meta_from_request
Request flow
""""""""""""
@@ -66,13 +66,19 @@ Vouchers
""""""""
.. automodule:: pretix.control.signals
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
:members: item_forms
Vouchers
""""""""
.. automodule:: pretix.control.signals
:members: voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards
""""""""""
.. automodule:: pretix.control.signals
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
:members: event_dashboard_widgets, user_dashboard_widgets
Ticket designs
""""""""""""""

View File

@@ -126,8 +126,6 @@ The provider class
.. autoattribute:: test_mode_message
.. autoattribute:: requires_invoice_immediately
Additional views
----------------

View File

@@ -61,7 +61,7 @@ A working example would be::
from pretix.base.plugins import PluginConfig
except ImportError:
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
class PaypalApp(PluginConfig):

View File

@@ -72,10 +72,6 @@ The output class
.. autoattribute:: download_button_icon
.. autoattribute:: multi_download_button_text
.. autoattribute:: long_download_button_text
.. autoattribute:: preview_allowed
.. autoattribute:: javascript_required

View File

@@ -7,7 +7,7 @@ Coding style and quality
for more information. Use four spaces for indentation.
* We sort our imports by a certain schema, but you don't have to do this by hand. Again, ``setup.cfg`` contains
some definitions that allow the command ``isort <directory>`` to automatically sort the imports in your source
some definitions that allow the command ``isort -rc <directory>`` to automatically sort the imports in your source
files.
* For templates and models, please take a look at the `Django Coding Style`_. We like Django's `class-based views`_ and
@@ -18,7 +18,7 @@ Coding style and quality
* We expect all new code to come with proper tests. When writing new tests, please write them using `pytest-style`_
test functions and raw ``assert`` statements. Use `fixtures`_ to prevent repetitive code. Some old parts of pretix'
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
but please use ``pytest`` style for any new test files.
but please use pytest style for any new test files.
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.

View File

@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
implementation could look like::
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
from pretix.base.signals import logentry_display
@receiver(signal=logentry_display)

View File

@@ -98,7 +98,7 @@ pull request nevertheless and ask us for help, we are happy to assist you.
Execute the following commands to check for code style errors::
flake8 .
isort -c .
isort -c -rc .
python manage.py check
Execute the following command to run pretix' test suite (might take a couple of minutes)::
@@ -121,7 +121,7 @@ for example, to check for any errors in any staged files when committing::
do
echo $file
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
git show ":$file" | isort -c - | grep ERROR && exit 1 || true
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
done

View File

@@ -1,356 +0,0 @@
Digital content
===============
URL interpolation and JWT authentication
----------------------------------------
In the simplest case, you can use the digital content module to point users to a specific piece of content on some
platform after their ticket purchase, or show them an embedded video or live stream. However, the full power of the
module can be utilized by passing additional information to the target system to automatically authenticate the user
or pre-fill some fields with their data. For example, you could use an URL like this::
https://webinars.example.com/join?as={attendee_name}&userid={order_code}-{positionid}
While this is already useful, it does not provide much security anyone could guess a valid combination for that URL.
Therefore, the module allows you to pass information as a `JSON Web Token`_, which isn't encrypted, but signed with a
shared secret such that nobody can create their own tokens or modify the contents. To use a token, set up a URL like this::
https://webinars.example.com/join?with_token={token}
Additionally, you will need to set a JWT secret and a token template, either through the pretix interface or through the
API (see below). pretix currently only supports tokens signed with ``HMAC-SHA256`` (``HS256``). Your token template can contain
whatever JSON you'd like to pass on based on the same variables, for example::
{
"iss": "pretix.eu",
"aud": "webinars.example.com",
"user": {
"id": "{order_code}-{positionid}",
"product": "{product_id}",
"variation": "{variation_id}",
"name": "{attendee_name}"
}
}
Variables can only be used in strings inside the JSON structure.
pretix will automatically add an ``iat`` claim with the current timestamp and an ``exp`` claim with an expiration timestamp
based on your configuration.
List of variables
"""""""""""""""""
The following variables are currently supported:
.. rst-class:: rest-resource-table
=================================== ====================================================================
Variable Description
=================================== ====================================================================
``order_code`` Order code (alphanumerical, unique per order, not per ticket)
``positionid`` ID of the ticket within the order (integer, starting at 1)
``order_email`` E-mail address of the ticket purchaser
``product_id`` Internal ID of the purchased product
``product_variation`` Internal ID of the purchased product variation (or empty)
``attendee_name`` Full name of the ticket holder (or empty)
``attendee_name_*`` Name parts of the ticket holder, depending on configuration, e.g. ``attendee_name_given_name`` or ``attendee_name_family_name``
``attendee_email`` E-mail address of the ticket holder (or empty)
``attendee_company`` Company of the ticket holder (or empty)
``attendee_street`` Street of the ticket holder's address (or empty)
``attendee_zipcode`` ZIP code of the ticket holder's address (or empty)
``attendee_city`` City of the ticket holder's address (or empty)
``attendee_country`` Country code of the ticket holder's address (or empty)
``attendee_state`` State of the ticket holder's address (or empty)
``answer[XYZ]`` Answer to the custom question with identifier ``XYZ``
``invoice_name`` Full name of the invoice address (or empty)
``invoice_name_*`` Name parts of the invoice address, depending on configuration, e.g. ``invoice_name_given_name`` or ``invoice_name_family_name``
``invoice_company`` Company of the invoice address (or empty)
``invoice_street`` Street of the invoice address (or empty)
``invoice_zipcode`` ZIP code of the invoice address (or empty)
``invoice_city`` City of the invoice address (or empty)
``invoice_country`` Country code of the invoice address (or empty)
``invoice_state`` State of the invoice address (or empty)
``meta_XYZ`` Value of the event's ``XYZ`` meta property
``token`` Signed JWT (only to be used in URLs, not in tokens)
=================================== ====================================================================
API Resource description
-------------------------
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
such as live streams, videos, or material downloads.
The digital content resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal content ID
title multi-lingual string The content title (required)
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
url string The location of the digital content
description multi-lingual string A public description of the item. May contain Markdown
syntax and is not required.
available_from datetime The first date time at which this content will be shown
(or ``null``).
available_until datetime The last date time at which this content will b e shown
(or ``null``).
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
position integer An integer, used for sorting
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
jwt_template string Template for JWT token generation
jwt_secret string Secret for JWT token generation
jwt_validity integer JWT validity in days
===================================== ========================== =======================================================
API Endpoints
-------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Returns a list of all digital content configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Returns information on one content item, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the content to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Create a new digital content.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to create new content for
:param event: The ``slug`` field of the event to create new content for
:statuscode 201: no error
:statuscode 400: The content 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 digital contents.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Update a content. 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.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"url": "https://mywebsite.com"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://mywebsite.com",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": 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 content to modify
:statuscode 200: no error
:statuscode 400: The content could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Delete a digital content.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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 content to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
.. _JSON Web Token: https://en.wikipedia.org/wiki/JSON_Web_Token

View File

@@ -15,5 +15,3 @@ If you want to **create** a plugin, please go to the
ticketoutputpdf
badges
campaigns
digital
webinar

View File

@@ -1,43 +0,0 @@
pretix Webinar
==============
Fetch host URLs
---------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/webinars/
Returns a list of all currently available webinar calls configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/webinars/ 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
[
{
"name": "Webinar B Sept. 8th, 2020",
"hosturl": "http://pretix.eu/demo/museum/webinar/host/a9aded3d7bd4df60/30611a34f9fee5d3/"
},
{
"name": "Webinar A Sept. 8, 2020",
"hosturl": "http://pretix.eu/demo/museum/webinar/host/e714x7d4a4a36a04/b9cc444665xxx757/"
}
]
:query subevent: Limit the result to the webinar(s) for a specific subevent.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -47,7 +47,6 @@ gunicorn
guid
hardcoded
hostname
ics
idempotency
iframe
incrementing
@@ -55,8 +54,6 @@ inofficial
invalidations
iterable
Jimdo
jwt
JWT
libpretixprint
libsass
linters

View File

@@ -26,9 +26,6 @@ Sender address
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.
Sender name
This is the name associated with the sender address. By default, this is your event name.
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.
@@ -36,15 +33,6 @@ Signature
Bcc address
This email address will receive a copy of every event-related email.
Attach calendar files
With this option, every order confirmation mail will include an ics file with name, date and location of
your event. It can be imported into many digital calendars.
Sales Channels for Checkout Emails
When you are using multiple sales channel, you may want to decide that mails for order and payment confirmation
are only to be sent for some sales channels. For orders created through the default online shop, these emails
must always be send. A similar option is available for ticket download reminders.
E-mail design
-------------

View File

@@ -1,94 +0,0 @@
.. _timeslots:
Use case: Time slots
====================
A more advanced use case of pretix is using pretix for time-slot-based access to an area with a limited visitor
capacity, such as a museum or other attraction. This guide will show you the quickest way to set up such an event
with pretix.
First of all, when creating your event, you need to select that your event represents an "event series":
.. thumbnail:: ../../../screens/event/create_step1.png
:align: center
:class: screenshot
You can click :ref:`here <subevents>` for a more general description of event series with pretix, but everything you
need to know is in this chapter as well.
General event setup
-------------------
Before you go further, set up your products that you want to sell for each time slot, such as different types of entry.
Creating slots
--------------
To create the time slots, you need to create a number of "dates" in the event series. Select "Dates" in the navigation
menu on the left side and click "Create many new dates". Then, first enter the pattern of your opening days. In the
example, the museum is open week Tuesday to Sunday. We recommend to create the slots for a few weeks at a time, but not
e.g. for a full year, since it will be more complicated to change things later.
.. thumbnail:: ../../../screens/event/timeslots_create.png
:align: center
:class: screenshot
Then, scroll to the times section and create your time slots. You can do any interval you like. If you have different
opening times on different week days, you will need to go through the creation process multiple times.
.. thumbnail:: ../../../screens/event/timeslots_create_2.png
:align: center
:class: screenshot
Scroll further down and create one or multiple quotas that define how many people can book a ticket for that time slot.
In this example, 50 people in total are allowed to enter within every slot:
.. thumbnail:: ../../../screens/event/timeslots_create_3.png
:align: center
:class: screenshot
Do **not** create a check-in list at this point. We will deal with this further below in the guide.
Now, press "Save" to create your slots.
.. warning:: If you create a lot of time slots at once, the server might need a few minutes to create them all in our
system. If you receive an error page because it took too long, please do not try again immediately but wait
for a few minutes. Most likely, the slots will be created successfully even though you saw an error.
Event settings
--------------
We recommend that you navigate to "Settings" > "General" > "Display" and set the settings "Default overview style"
to "Week calendar":
.. thumbnail:: ../../../screens/event/timeslots_settings_1.png
:align: center
:class: screenshot
Now, your ticket shop should give users a nice weekly overview over all time slots and their availability:
.. thumbnail:: ../../../screens/event/timeslots_presale.png
:align: center
:class: screenshot
Check-in
--------
If you want to scan people at the entrance of your event and only admit them at their designated time, we recommend
the following setup: Go to "Check-in" in the main navigation on the left and create a new check-in list. Give it a name
and do *not* choose a specific data. We will use one check-in list for all dates. Then, go to the "Advanced" tab at
the top and set up two restrictions to make sure people can only get in during the time slot they registered for.
You can create the rules exactly like shown in the following screenshot:
.. thumbnail:: ../../../screens/event/timeslots_checkinlists.png
:align: center
:class: screenshot
If you want, you can enter a tolerance of e.g. "10" if you want to be a little bit more relaxed and admit people up to
10 minutes before or after their time slot.
Now, download our `Android or Desktop app`_ and register it to your account. The app will ask you to select one the
time slots, but it does not matter, you can select any one of them and then select your newly created check-in list.
That's it, you're good to go!
.. _Android or Desktop app: https://pretix.eu/about/en/scan

View File

@@ -292,8 +292,6 @@ Flexible group sizes
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
For more complex use cases, you can also use add-on products that can be chosen multiple times.
This way, your ticket can be bought an arbitrary number of times but no less than the given minimal amount per order.
Fixed group sizes
@@ -346,13 +344,3 @@ In addition to your normal conference quota, you need to create an unlimited quo
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
You can find more use cases in these specialized guides:
More use cases
--------------
.. toctree::
:maxdepth: 1
guides/timeslots

View File

@@ -114,17 +114,6 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
Filtering products
------------------
You can filter the products shown in the widget by passing in a list of product IDs::
<pretix-widget event="https://pretix.eu/demo/democon/" items="23,42"></pretix-widget>
Alternatively, you can select one or more categories to be shown::
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
Multi-event selection
---------------------
@@ -136,15 +125,10 @@ If you want to include all your public events, you can just reference your organ
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
There is an optional ``style`` parameter that let's you choose between a monthly calendar view, a week view and a list
view. If you do not set it, the choice will be taken from your organizer settings::
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="week"></pretix-widget>
If you have more than 100 events, the system might refuse to show a list view and always show a calendar for performance
reasons instead.
You can see an example here:

View File

@@ -58,6 +58,28 @@ method without creating a new order. If payment deadlines were dependent on the
forth could either allow someone to extend their deadline forever, or render someones order invalid by moving the date
back in the past.
How can I revert a check-in?
----------------------------
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
inconvenient for some of you, but we have a good reason for it:
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
scans were made and uploaded (which could be two different orders).
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
future, but we're not there yet.
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
delete and re-create the check-in list.
Why does pretix not support any 1D (linear) bar codes?
------------------------------------------------------

View File

@@ -14,23 +14,30 @@ and with pretix, you can do this. On this page, you find out the necessary steps
With the pretix.eu hosted service
---------------------------------
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
This page will show you instructions on how to set up your own domain. Basically, it works like this:
Step 1: DNS Configuration
#########################
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
domain provider.
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
The value of the record should be ``www.pretix.eu``.
Step 2: Wait for the DNS entry to propagate
###########################################
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
Now, tell us about your domain on the "Custom Domain" page to get started.
Step 3: Tell us
###############
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
certificate for you (for free!) and configure the domain.
With a custom pretix installation
---------------------------------

View File

@@ -1,8 +1,8 @@
General settings
================
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The "Deadline"
and "Advanced" tabs of the page show a number of general settings that affect all payment methods:
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
of the page shows a number of general settings that affect all payment methods:
.. thumbnail:: ../../screens/event/settings_payment.png
:align: center

View File

@@ -3,7 +3,6 @@ include README.rst
recursive-include pretix/static *
recursive-include pretix/static.dist *
recursive-include pretix/locale *
recursive-include pretix/helpers/locale *
recursive-include pretix/base/templates *
recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *

View File

@@ -1 +1 @@
__version__ = "3.11.1"
__version__ = "3.6.0.post1"

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from oauth2_provider.generators import (
generate_client_id, generate_client_secret,
)

View File

@@ -2,7 +2,7 @@ from datetime import timedelta
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
from django.utils.translation import ugettext_lazy
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else:
validated_data['seat'] = seat
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')

View File

@@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -14,19 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for exclude_field in self.context['request'].query_params.getlist('exclude'):
p = exclude_field.split('.')
if p[0] in self.fields:
if len(p) == 1:
del self.fields[p[0]]
elif len(p) == 2:
self.fields[p[0]].child.fields.pop(p[1])
'include_pending', 'auto_checkin_sales_channels')
def validate(self, data):
data = super().validate(data)
@@ -40,7 +28,9 @@ class CheckinListSerializer(I18nAwareModelSerializer):
raise ValidationError(_('One or more items do not belong to this event.'))
if event.has_subevents:
if full_data.get('subevent') and event != full_data.get('subevent').event:
if not full_data.get('subevent'):
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != full_data.get('subevent').event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if full_data.get('subevent'):

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
from django_countries.serializers import CountryFieldMixin
from hierarkey.proxy import HierarkeyProxy
from pytz import common_timezones
@@ -29,29 +29,11 @@ class MetaDataField(Field):
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()):
raise ValidationError('meta_data needs to be an object (str -> str).')
return {
'meta_data': data
}
class MetaPropertyField(Field):
def to_representation(self, value):
return {
v.name: v.default for v in value.item_meta_properties.all()
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, str) for k in data.values()):
raise ValidationError('item_meta_properties needs to be an object (str -> str).')
return {
'item_meta_properties': data
}
class SeatCategoryMappingField(Field):
def to_representation(self, value):
@@ -63,8 +45,6 @@ class SeatCategoryMappingField(Field):
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, int) for k in data.values()):
raise ValidationError('seat_category_mapping needs to be an object (str -> int).')
return {
'seat_category_mapping': data or {}
}
@@ -97,7 +77,6 @@ class TimeZoneField(ChoiceField):
class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
item_meta_properties = MetaPropertyField(required=False, source='*')
plugins = PluginsField(required=False, source='*')
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
@@ -107,7 +86,7 @@ class EventSerializer(I18nAwareModelSerializer):
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
'plugins', 'seat_category_mapping', 'timezone')
def validate(self, data):
data = super().validate(data)
@@ -152,12 +131,6 @@ class EventSerializer(I18nAwareModelSerializer):
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
return value
@cached_property
def item_meta_props(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_seating_plan(self, value):
if value and value.organizer != self.context['request'].organizer:
raise ValidationError('Invalid seating plan.')
@@ -199,7 +172,6 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
validated_data.pop('seat_category_mapping', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
tz = validated_data.pop('timezone', None)
@@ -216,15 +188,6 @@ class EventSerializer(I18nAwareModelSerializer):
value=value
)
# Item Meta properties
if item_meta_properties is not None:
for key, value in item_meta_properties.items():
event.item_meta_properties.create(
name=key,
default=value,
event=event
)
# Seats
if event.seating_plan:
generate_seats(event, None, event.seating_plan, {})
@@ -239,7 +202,6 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
plugins = validated_data.pop('plugins', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
tz = validated_data.pop('timezone', None)
@@ -266,26 +228,6 @@ class EventSerializer(I18nAwareModelSerializer):
if prop.name not in meta_data:
current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
current = [imp for imp in event.item_meta_properties.all()]
for key, value in item_meta_properties.items():
prop = self.item_meta_props.get(key)
if prop in current:
prop.default = value
prop.save()
else:
prop = event.item_meta_properties.create(
name=key,
default=value,
event=event
)
current.append(prop)
for prop in current:
if prop.name not in list(item_meta_properties.keys()):
prop.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = {
@@ -348,13 +290,13 @@ class CloneEventSerializer(EventSerializer):
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price', 'disabled')
fields = ('item', 'price')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price', 'disabled')
fields = ('variation', 'price')
class SubEventSerializer(I18nAwareModelSerializer):
@@ -459,29 +401,27 @@ class SubEventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def update(self, instance, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set', None)
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set', None)
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
subevent = super().update(instance, validated_data)
if item_price_overrides_data is not None:
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
for item_price_override_data in item_price_overrides_data:
id = existing_item_overrides.pop(item_price_override_data['item'], None)
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
for item_price_override_data in item_price_overrides_data:
id = existing_item_overrides.pop(item_price_override_data['item'], None)
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
if variation_price_overrides_data is not None:
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
for variation_price_override_data in variation_price_overrides_data:
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
for variation_price_override_data in variation_price_overrides_data:
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
# Meta data
if meta_data is not None:
@@ -541,9 +481,6 @@ class EventSettingsSerializer(serializers.Serializer):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
'show_items_outside_presale_period',
@@ -564,23 +501,16 @@ class EventSettingsSerializer(serializers.Serializer):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'confirm_texts',
'confirm_text',
'order_email_asked_twice',
'payment_term_mode',
'payment_term_days',
'payment_term_weekdays',
'payment_term_minutes',
'payment_term_last',
'payment_term_weekdays',
'payment_term_expire_automatically',
'payment_term_accept_late',
'payment_explanation',
@@ -598,7 +528,6 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_show_payments',
@@ -608,7 +537,6 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations',
'invoice_numbers_counter_length',
'invoice_attendee_name',
'invoice_include_expire_date',
'invoice_address_explanation_text',
@@ -623,7 +551,6 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_introductory_text',
'invoice_additional_text',
'invoice_footer_text',
'invoice_eu_currencies',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_paid',
@@ -631,13 +558,6 @@ class EventSettingsSerializer(serializers.Serializer):
'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
'change_allow_user_until',
'change_allow_user_price',
]
def __init__(self, *args, **kwargs):
@@ -645,13 +565,9 @@ class EventSettingsSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
@@ -680,40 +596,3 @@ class EventSettingsSerializer(serializers.Serializer):
settings_dict.update(data)
validate_settings(self.event, settings_dict)
return data
class DeviceEventSettingsSerializer(EventSettingsSerializer):
default_fields = [
'locales',
'locale',
'last_order_modification_date',
'show_quota_left',
'max_items_per_order',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'ticket_download',
'ticket_download_addons',
'ticket_download_nonadm',
'ticket_download_pending',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_address_from_name',
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
]

View File

@@ -1,29 +0,0 @@
from collections import OrderedDict
from rest_framework import serializers
def remove_duplicates_from_list(data):
return list(OrderedDict.fromkeys(data))
class ListMultipleChoiceField(serializers.MultipleChoiceField):
def to_internal_value(self, data):
if isinstance(data, str) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
internal_value_data = [
super(serializers.MultipleChoiceField, self).to_internal_value(item)
for item in data
]
return remove_duplicates_from_list(internal_value_data)
def to_representation(self, value):
representation_data = [
self.choice_strings_to_values.get(str(item), item) for item in value
]
return remove_duplicates_from_list(representation_data)

View File

@@ -2,15 +2,13 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
Question, QuestionOption, Quota,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
QuestionOption, Quota,
)
@@ -45,7 +43,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('addon_category', 'min_count', 'max_count',
'position', 'price_included', 'multi_allowed')
'position', 'price_included')
class ItemBundleSerializer(serializers.ModelSerializer):
@@ -77,7 +75,7 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('id', 'addon_category', 'min_count', 'max_count',
'position', 'price_included', 'multi_allowed')
'position', 'price_included')
def validate(self, data):
data = super().validate(data)
@@ -112,7 +110,6 @@ class ItemSerializer(I18nAwareModelSerializer):
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
class Meta:
model = Item
@@ -122,7 +119,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
read_only_fields = ('has_variations', 'picture')
def validate(self, data):
@@ -170,65 +167,18 @@ class ItemSerializer(I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
return value
@cached_property
def item_meta_properties(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.item_meta_properties:
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
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 {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
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)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
ItemMetaValue.objects.create(
property=self.item_meta_properties.get(key),
value=value,
item=item
)
return item
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
item = super().update(instance, validated_data)
# Meta data
if meta_data is not None:
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.item_meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
item.meta_values.create(
property=self.item_meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
return item
@@ -287,8 +237,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
if value:
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
if value == self.instance:
raise ValidationError('A question cannot depend on itself.')
if value == self.instance:
raise ValidationError('A question cannot depend on itself.')
return value
def validate(self, data):
@@ -349,7 +299,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
def validate(self, data):
data = super().validate(data)

View File

@@ -5,7 +5,7 @@ from decimal import Decimal
import pycountry
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
from django.utils.translation import ugettext_lazy
from django_countries.fields import Country
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -25,7 +25,6 @@ from pretix.base.models.orders import (
)
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
@@ -39,7 +38,7 @@ class CompatibleCountryField(serializers.Field):
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
elif hasattr(instance, 'country_old'):
else:
return instance.country_old
@@ -68,7 +67,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
@@ -97,11 +96,6 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
return [o.identifier for o in instance.options.all()]
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class InlineSeatSerializer(I18nAwareModelSerializer):
class Meta:
@@ -112,7 +106,6 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
options = AnswerQuestionOptionsField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
@@ -122,7 +115,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list', 'auto_checked_in', 'type')
fields = ('datetime', 'list', 'auto_checked_in')
class OrderDownloadsField(serializers.Field):
@@ -196,11 +189,6 @@ class PdfDataSerializer(serializers.Field):
for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v
if not hasattr(instance.item, '_cached_meta_data'):
instance.item._cached_meta_data = instance.item.meta_data
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
return res
@@ -211,12 +199,10 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*')
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
@@ -270,9 +256,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
'order__status')
@@ -377,14 +362,6 @@ class OrderSerializer(I18nAwareModelSerializer):
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields['positions'].child.fields.pop('pdf_data')
for exclude_field in self.context['request'].query_params.getlist('exclude'):
p = exclude_field.split('.')
if p[0] in self.fields:
if len(p) == 1:
del self.fields[p[0]]
elif len(p) == 2:
self.fields[p[0]].child.fields.pop(p[1])
def validate_locale(self, l):
if l not in set(k for k in self.instance.event.settings.locales):
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
@@ -425,26 +402,16 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
class SimulatedOrderSerializer(OrderSerializer):
positions = SimulatedOrderPositionSerializer(many=True, read_only=True)
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
tax_rule = serializers.PrimaryKeyRelatedField(queryset=TaxRule.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['tax_rule'].queryset = event.tax_rules.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
@@ -537,22 +504,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
country = CompatibleCountryField(source='*')
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for k, v in self.fields.items():
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
v.required = False
v.allow_blank = True
v.allow_null = True
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError(
@@ -607,24 +564,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
@@ -641,28 +580,6 @@ class CompatibleJSONField(serializers.JSONField):
return value
class WrappedList:
def __init__(self, data):
self._data = data
def all(self):
return self._data
class WrappedModel:
def __init__(self, model):
self._wrapped = model
def __getattr__(self, item):
return getattr(self._wrapped, item)
def save(self, *args, **kwargs):
raise NotImplementedError
def delete(self, *args, **kwargs):
raise NotImplementedError
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=True)
@@ -683,7 +600,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -693,7 +609,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail', 'simulate')
'force', 'send_mail')
def validate_payment_provider(self, pp):
if pp is None:
@@ -770,7 +686,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
if testmode and not sales_channel.testmode_supported:
raise ValidationError('This sales channel does not provide support for test mode.')
raise ValidationError('This sales channel does not provide support for testmode.')
except KeyError:
# We do not need to raise a ValidationError here, since there is another check to validate the
# sales_channel
@@ -785,7 +701,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data:
@@ -799,10 +714,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
ia = None
lockfn = self.context['event'].lock
if simulate:
lockfn = NoLockManager
with lockfn() as now_dt:
with self.context['event'].lock() as now_dt:
free_seats = set()
seats_seen = set()
consume_carts = validated_data.pop('consume_carts', [])
@@ -911,7 +823,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
@@ -922,24 +834,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
if pos_data.get('subevent'):
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
if (
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
):
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
@@ -951,7 +850,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
@@ -965,20 +864,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
if simulate:
order = WrappedModel(order)
order.last_modified = now()
order.code = 'PREVIEW'
else:
order.save()
order.save()
if ia:
if not simulate:
ia.order = order
ia.save()
else:
order.invoice_address = ia
ia.last_modified = now()
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
@@ -990,15 +880,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'_legacy': attendee_name
}
pos = OrderPosition(**pos_data)
if simulate:
pos.order = order._wrapped
else:
pos.order = order
pos.order = order
if addon_to:
if simulate:
pos.addon_to = pos_map[addon_to]._wrapped
else:
pos.addon_to = pos_map[addon_to]
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(
@@ -1027,33 +911,19 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address=ia,
).gross
if simulate:
pos = WrappedModel(pos)
pos.id = 0
answers = []
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = WrappedModel(QuestionAnswer(**answ_data))
answ.options = WrappedList(options)
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
else:
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if not simulate:
for cp in delete_cps:
cp.delete()
for cp in delete_cps:
cp.delete()
order.total = sum([p.price for p in pos_map.values()])
fees = []
order.total = sum([p.price for p in order.positions.all()])
for fee_data in fees_data:
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
if is_percentage:
@@ -1085,26 +955,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fee_data['tax_rule'] = tr
fee_data['value'] = val
f = OrderFee(**fee_data)
f.order = order._wrapped if simulate else order
f.order = order
f._calculate_tax()
fees.append(f)
if not simulate:
f.save()
f.save()
else:
f = OrderFee(**fee_data)
f.order = order._wrapped if simulate else order
f.order = order
f._calculate_tax()
fees.append(f)
if not simulate:
f.save()
f.save()
order.total += sum([f.value for f in fees])
if simulate:
order.fees = fees
order.positions = pos_map.values()
return order # ignore payments
else:
order.save(update_fields=['total'])
order.total += sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'

View File

@@ -1,7 +1,7 @@
from decimal import Decimal
from django.db.models import Q
from django.utils.translation import get_language, gettext_lazy as _
from django.utils.translation import get_language, ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -55,7 +55,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
class EventSlugField(serializers.SlugRelatedField):

View File

@@ -6,7 +6,6 @@ from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import periodic_task
from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal(
providing_args=[]
@@ -20,13 +19,11 @@ instances.
@receiver(periodic_task)
@scopes_disabled()
@minimum_interval(minutes_after_success=12 * 60)
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
@receiver(periodic_task)
@scopes_disabled()
@minimum_interval(minutes_after_success=12 * 60)
def cleanup_api_logs(sender, **kwargs):
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()

View File

@@ -41,8 +41,8 @@ class ConditionalListView:
return super().list(request, **kwargs)
lmd = request.event.logentry_set.filter(
content_type__model=self.get_queryset().model._meta.model_name,
content_type__app_label=self.get_queryset().model._meta.app_label,
content_type__model=self.queryset.model._meta.model_name,
content_type__app_label=self.queryset.model._meta.app_label,
).aggregate(
m=Max('datetime')
)['m']

View File

@@ -1,4 +1,3 @@
import django_filters
from django.core.exceptions import ValidationError
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
from django.db.models.functions import Coalesce
@@ -28,17 +27,10 @@ from pretix.helpers.database import FixedOrderBy
with scopes_disabled():
class CheckinListFilter(FilterSet):
subevent_match = django_filters.NumberFilter(method='subevent_match_qs')
class Meta:
model = CheckinList
fields = ['subevent']
def subevent_match_qs(self, qs, name, value):
return qs.filter(
Q(subevent_id=value) | Q(subevent_id__isnull=True)
)
class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
@@ -96,9 +88,8 @@ class CheckinListViewSet(viewsets.ModelViewSet):
pqs = OrderPosition.objects.filter(
order__event=clist.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
subevent=clist.subevent,
)
if clist.subevent:
pqs = pqs.filter(subevent=clist.subevent)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
@@ -200,7 +191,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except ValueError:
raise Http404()
def get_queryset(self, ignore_status=False, ignore_products=False):
def get_queryset(self, ignore_status=False):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
@@ -210,13 +201,10 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
)
if self.checkinlist.subevent:
qs = qs.filter(
subevent=self.checkinlist.subevent
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
@@ -255,40 +243,23 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if not self.checkinlist.all_products and not ignore_products:
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
return qs
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>[^/]+)/redeem')
@action(detail=True, methods=['POST'])
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
if type not in dict(Checkin.CHECKIN_TYPES):
raise ValidationError("Invalid check-in type.")
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object(ignore_status=True)
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
else:
dt = now()
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
if self.kwargs['pk'].isnumeric():
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
raise Http404()
given_answers = {}
if 'answers' in self.request.data:
aws = self.request.data.get('answers')
@@ -312,7 +283,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
canceled_supported=self.request.data.get('canceled_supported', False),
user=self.request.user,
auth=self.request.auth,
type=type,
)
except RequiredQuestionsError as e:
return Response({
@@ -324,14 +294,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
]
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'datetime': dt,
'type': type,
'list': self.checkinlist.pk
}, user=self.request.user, auth=self.request.auth)
return Response({
'status': 'error',
'reason': e.code,
@@ -344,3 +306,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self, ignore_status=False):
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
obj = get_object_or_404(queryset, secret=self.kwargs['pk'])
return obj

View File

@@ -10,8 +10,8 @@ from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -337,16 +337,10 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'
permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs):
if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
elif 'can_change_event_settings' in request.eventpermset:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
else:
raise PermissionDenied()
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
if 'explain' in request.GET:
return Response({
fname: {

View File

@@ -20,7 +20,6 @@ from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
@@ -534,18 +533,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
def availability(self, request, *args, **kwargs):
quota = self.get_object()
qa = QuotaAvailability()
qa.queue(quota)
qa.compute()
avail = qa.results[quota]
avail = quota.availability()
data = {
'paid_orders': qa.count_paid_orders[quota],
'pending_orders': qa.count_pending_orders[quota],
'exited_orders': qa.count_exited_orders[quota],
'blocking_vouchers': qa.count_vouchers[quota],
'cart_positions': qa.count_cart[quota],
'waiting_list': qa.count_pending_orders[quota],
'paid_orders': quota.count_paid_orders(),
'pending_orders': quota.count_pending_orders(),
'blocking_vouchers': quota.count_blocking_vouchers(),
'cart_positions': quota.count_in_cart(),
'waiting_list': quota.count_waiting_list_pending(),
'available_number': avail[1],
'available': avail[0] == Quota.AVAILABILITY_OK,
'total_size': quota.size,

View File

@@ -2,7 +2,7 @@ import logging
from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.forms import AllowForm
from oauth2_provider.views import (

View File

@@ -4,12 +4,12 @@ from decimal import Decimal
import django_filters
import pytz
from django.db import transaction
from django.db.models import Exists, F, OuterRef, Prefetch, Q
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, viewsets
@@ -26,12 +26,12 @@ from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
OrderPaymentSerializer, OrderPositionSerializer,
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
PriceCalcSerializer, SimulatedOrderSerializer,
PriceCalcSerializer,
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
TeamAPIToken, generate_position_secret, generate_secret,
)
from pretix.base.payment import PaymentException
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate
@@ -52,7 +52,6 @@ from pretix.base.signals import (
order_modified, order_paid, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
with scopes_disabled():
class OrderFilter(FilterSet):
@@ -61,62 +60,11 @@ with scopes_disabled():
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
search = django_filters.CharFilter(method='search_qs')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
def subevent_after_qs(self, qs, name, value):
qs = qs.annotate(
has_se_after=Exists(
OrderPosition.all.filter(
subevent_id__in=SubEvent.objects.filter(
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True), event=OuterRef(OuterRef('event_id'))
).values_list('id'),
order_id=OuterRef('pk'),
)
)
).filter(has_se_after=True)
return qs
def search_qs(self, qs, name, value):
u = value
if "-" in value:
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q(
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
)
).values('id')
mainq = (
code
| Q(email__icontains=u)
| Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
mainq = mainq | q
return qs.annotate(has_pos=Exists(matching_positions)).filter(
mainq
)
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
@@ -135,19 +83,16 @@ class OrderViewSet(viewsets.ModelViewSet):
return ctx
def get_queryset(self):
qs = self.request.event.orders
if 'fees' not in self.request.GET.getlist('exclude'):
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
else:
fqs = OrderFee.objects
qs = qs.prefetch_related(Prefetch('fees', queryset=fqs.all()))
if 'payments' not in self.request.GET.getlist('exclude'):
qs = qs.prefetch_related('payments')
if 'refunds' not in self.request.GET.getlist('exclude'):
qs = qs.prefetch_related('refunds', 'refunds__payment')
if 'invoice_address' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('invoice_address')
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
else:
fqs = OrderFee.objects
qs = self.request.event.orders.prefetch_related(
Prefetch('fees', queryset=fqs.all()),
'payments', 'refunds', 'refunds__payment'
).select_related(
'invoice_address'
)
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
opq = OrderPosition.all
@@ -184,7 +129,6 @@ class OrderViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset())
@@ -317,29 +261,6 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def reactivate(self, request, **kwargs):
order = self.get_object()
if order.status != Order.STATUS_CANCELED:
return Response(
{'detail': 'The order is not allowed to be reactivated.'},
status=status.HTTP_400_BAD_REQUEST
)
try:
reactivate_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
)
except OrderError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
@@ -544,12 +465,7 @@ class OrderViewSet(viewsets.ModelViewSet):
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
if not order.pk:
# Simulation
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
serializer = OrderSerializer(order, context=serializer.context)
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
@@ -801,8 +717,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
{
"item": 2,
"variation": null,
"subevent": 3,
"tax_rule": 4,
"subevent": 3
}
Sample output:
@@ -856,11 +771,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
if data.get('tax_rule'):
kwargs['tax_rule'] = data.get('tax_rule')
price = get_price(**kwargs)
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
@@ -869,7 +780,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
'tax_rule': tr.pk if tr else None,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
@@ -1168,14 +1078,11 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=request.auth
)
if mark_refunded:
try:
mark_order_refunded(
r.order,
user=request.user if request.user.is_authenticated else None,
auth=(request.auth if request.auth else None),
)
except OrderError as e:
raise ValidationError(str(e))
mark_order_refunded(
r.order,
user=request.user if request.user.is_authenticated else None,
auth=(request.auth if request.auth else None),
)
elif mark_pending:
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
r.order.status = Order.STATUS_PENDING

View File

@@ -1,11 +1,8 @@
from decimal import Decimal
import django_filters
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
@@ -58,7 +55,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_organizer_settings'
def get_queryset(self):
return self.request.organizer.seating_plans.order_by('name')
return self.request.organizer.seating_plans.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -101,29 +98,14 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
instance.delete()
with scopes_disabled():
class GiftCardFilter(FilterSet):
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
class Meta:
model = GiftCard
fields = ['secret', 'testmode']
class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
filter_backends = (DjangoFilterBackend,)
filterset_class = GiftCardFilter
def get_queryset(self):
if self.request.GET.get('include_accepted') == 'true':
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return qs
return self.request.organizer.issued_gift_cards.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -144,8 +126,6 @@ class GiftCardViewSet(viewsets.ModelViewSet):
@transaction.atomic()
def perform_update(self, serializer):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
@@ -168,19 +148,16 @@ class GiftCardViewSet(viewsets.ModelViewSet):
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value')
)
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
if gc.value + value < Decimal('0.00'):
return Response({
'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text)
gc.transactions.create(value=value)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value, 'text': text}
data={'value': value}
)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
@@ -195,7 +172,7 @@ class TeamViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_teams'
def get_queryset(self):
return self.request.organizer.teams.order_by('pk')
return self.request.organizer.teams.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -268,7 +245,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.invites.order_by('email')
return self.team.invites.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -305,7 +282,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.tokens.order_by('name')
return self.team.tokens.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()

View File

@@ -7,7 +7,7 @@ import requests
from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django_scopes import scope, scopes_disabled
from requests import RequestException
@@ -125,10 +125,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.canceled',
_('Order canceled'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.reactivated',
_('Order reactivated'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.expired',
_('Order expired'),
@@ -168,9 +164,9 @@ def register_default_webhook_events(sender, **kwargs):
)
@app.task(base=TransactionAwareTask, acks_late=True)
@app.task(base=TransactionAwareTask)
def notify_webhooks(logentry_id: int):
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.organizer:
return # We need to know the organizer
@@ -205,7 +201,7 @@ def notify_webhooks(logentry_id: int):
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
with scopes_disabled():

View File

@@ -85,16 +85,6 @@ class BaseAuthBackend:
"""
return
def get_next_url(self, request):
"""
This method will be called after a successful login to determine the next URL. Pretix in general uses the
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
"""
if "next" in request.GET:
return request.GET.get("next")
return None
class NativeAuthBackend(BaseAuthBackend):
identifier = 'native'

View File

@@ -2,7 +2,7 @@ import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import register_sales_channels

View File

@@ -9,12 +9,10 @@ from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from django.utils.translation import get_language, ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
from pretix.base.models import Event
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
@@ -260,40 +258,15 @@ def _placeholder_payment(order, payment):
return str(payment.payment_provider.order_pending_mail_render(order))
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.base.models import InvoiceAddress
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
@@ -306,19 +279,15 @@ def base_placeholders(sender, **kwargs):
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
# TODO: This used to be "date" in some placeholders, add a migration!
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
@@ -337,51 +306,6 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
@@ -496,7 +420,11 @@ def base_placeholders(sender, **kwargs):
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
lambda position_or_address: (
position_or_address.name
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name
),
_('John Doe'),
),
]
@@ -511,7 +439,11 @@ def base_placeholders(sender, **kwargs):
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_best_name(position_or_address, parts=True).get(f, ''),
lambda position_or_address, f=f: (
position_or_address.name_parts.get(f, '')
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name_parts.get(f, '')
),
name_scheme['sample'][f]
))

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