forked from CGM_Public/pretix_original
Compare commits
118
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8113d9471d | ||
|
|
20edbe47d6 | ||
|
|
9b6e7f369c | ||
|
|
3ba36512d2 | ||
|
|
d7d419c826 | ||
|
|
acb35866ba | ||
|
|
11a04acae8 | ||
|
|
b882d84678 | ||
|
|
8e0ad694b7 | ||
|
|
39a0eaff3e | ||
|
|
7b3eeaf411 | ||
|
|
3f536c2c7a | ||
|
|
36203d653b | ||
|
|
fae45d066a | ||
|
|
5de5642b59 | ||
|
|
5991a71316 | ||
|
|
da09baa712 | ||
|
|
271d58d2d9 | ||
|
|
400967c9e2 | ||
|
|
b37947a94a | ||
|
|
f86e869702 | ||
|
|
43a27dc0b0 | ||
|
|
2e475432f6 | ||
|
|
03dfeab03f | ||
|
|
e86a53a763 | ||
|
|
99deb7a78c | ||
|
|
6c69bc6a78 | ||
|
|
9f6b9f03e6 | ||
|
|
d9b8d61ac6 | ||
|
|
6eff5d0cf8 | ||
|
|
d2a6c6600d | ||
|
|
1f01ab232e | ||
|
|
c74bd17f59 | ||
|
|
980d9a1671 | ||
|
|
0ac5290e90 | ||
|
|
c7a9ff52e3 | ||
|
|
111598db7a | ||
|
|
be53751fc7 | ||
|
|
8d07e2dc92 | ||
|
|
c2a42db9cd | ||
|
|
bedd841063 | ||
|
|
543b089acf | ||
|
|
575858aec6 | ||
|
|
6ceb5bd7ce | ||
|
|
162fd9e656 | ||
|
|
ae73370a3e | ||
|
|
5f6ec5e771 | ||
|
|
bf548904f3 | ||
|
|
f280eedc21 | ||
|
|
57b5036f36 | ||
|
|
545d911219 | ||
|
|
c58cfb59ad | ||
|
|
56803e3d65 | ||
|
|
7d8a788361 | ||
|
|
ecb4c34e08 | ||
|
|
75abab403a | ||
|
|
dea7de4e6c | ||
|
|
af3100a354 | ||
|
|
66ae5d1af2 | ||
|
|
9efb1653ee | ||
|
|
c6f1441976 | ||
|
|
7d08337c49 | ||
|
|
c92a1eb641 | ||
|
|
babe0934a8 | ||
|
|
160e0e0531 | ||
|
|
183387a683 | ||
|
|
cea0ac59bf | ||
|
|
3b6f116f5d | ||
|
|
e884c9820f | ||
|
|
7545e92373 | ||
|
|
b16680e0e5 | ||
|
|
eb04fdf4d2 | ||
|
|
c842ea597c | ||
|
|
b2b3fa36be | ||
|
|
af50bc2fa4 | ||
|
|
734a116b6f | ||
|
|
8f36e36619 | ||
|
|
2f35e1fcc6 | ||
|
|
cdfc2190d5 | ||
|
|
8dbc7ac5d7 | ||
|
|
b04ec720dc | ||
|
|
1478f191a8 | ||
|
|
171b1de0c4 | ||
|
|
799f1a5388 | ||
|
|
3cdb61fcf9 | ||
|
|
e5ce2deaeb | ||
|
|
f986f20f6a | ||
|
|
8f22d52e24 | ||
|
|
1c944baafa | ||
|
|
e55e3eeb0c | ||
|
|
063967528f | ||
|
|
edbe42c524 | ||
|
|
50a855a964 | ||
|
|
1ad81d21a4 | ||
|
|
0f8fa3fe47 | ||
|
|
45b9a705e2 | ||
|
|
3f07050d42 | ||
|
|
f97e6d0a71 | ||
|
|
c0031e4579 | ||
|
|
ce73d4831e | ||
|
|
fbfae0838a | ||
|
|
5445a79572 | ||
|
|
97a03f0aae | ||
|
|
780a5cdeee | ||
|
|
991f245dce | ||
|
|
8913d35838 | ||
|
|
7f2616cf80 | ||
|
|
7d9b54709a | ||
|
|
231e05f967 | ||
|
|
e50db7844b | ||
|
|
b43523ea65 | ||
|
|
447370d7b3 | ||
|
|
6a36efd18c | ||
|
|
c1ebbfe82a | ||
|
|
7f36a80dd8 | ||
|
|
c52b998ea8 | ||
|
|
eecd002ffd | ||
|
|
53027b621f |
@@ -66,6 +66,10 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./src
|
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
|
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: Run concurrency tests
|
||||||
|
working-directory: ./src
|
||||||
|
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
||||||
|
if: matrix.database == 'postgres'
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
+12
-12
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-bullseye
|
FROM python:3.11-bookworm
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
@@ -20,20 +20,20 @@ RUN apt-get update && \
|
|||||||
supervisor \
|
supervisor \
|
||||||
libmaxminddb0 \
|
libmaxminddb0 \
|
||||||
libmaxminddb-dev \
|
libmaxminddb-dev \
|
||||||
zlib1g-dev && \
|
zlib1g-dev \
|
||||||
|
nodejs \
|
||||||
|
npm && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
dpkg-reconfigure locales && \
|
dpkg-reconfigure locales && \
|
||||||
locale-gen C.UTF-8 && \
|
locale-gen C.UTF-8 && \
|
||||||
/usr/sbin/update-locale LANG=C.UTF-8 && \
|
/usr/sbin/update-locale LANG=C.UTF-8 && \
|
||||||
mkdir /etc/pretix && \
|
mkdir /etc/pretix && \
|
||||||
mkdir /data && \
|
mkdir /data && \
|
||||||
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
|
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:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||||
mkdir /static && \
|
mkdir /static && \
|
||||||
mkdir /etc/supervisord && \
|
mkdir /etc/supervisord
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
|
|
||||||
ENV LC_ALL=C.UTF-8 \
|
ENV LC_ALL=C.UTF-8 \
|
||||||
@@ -63,10 +63,10 @@ RUN pip3 install -U \
|
|||||||
RUN chmod +x /usr/local/bin/pretix && \
|
RUN chmod +x /usr/local/bin/pretix && \
|
||||||
rm /etc/nginx/sites-enabled/default && \
|
rm /etc/nginx/sites-enabled/default && \
|
||||||
cd /pretix/src && \
|
cd /pretix/src && \
|
||||||
rm -f pretix.cfg && \
|
rm -f pretix.cfg && \
|
||||||
mkdir -p data && \
|
mkdir -p data && \
|
||||||
chown -R pretixuser:pretixuser /pretix /data data && \
|
chown -R pretixuser:pretixuser /pretix /data data && \
|
||||||
sudo -u pretixuser make production
|
sudo -u pretixuser make production
|
||||||
|
|
||||||
USER pretixuser
|
USER pretixuser
|
||||||
VOLUME ["/etc/pretix", "/data"]
|
VOLUME ["/etc/pretix", "/data"]
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ Example::
|
|||||||
password=abcd
|
password=abcd
|
||||||
host=localhost
|
host=localhost
|
||||||
port=3306
|
port=3306
|
||||||
|
advisory_lock_index=1
|
||||||
sslmode=require
|
sslmode=require
|
||||||
sslrootcert=/etc/pretix/postgresql-ca.crt
|
sslrootcert=/etc/pretix/postgresql-ca.crt
|
||||||
sslcert=/etc/pretix/postgresql-client-crt.crt
|
sslcert=/etc/pretix/postgresql-client-crt.crt
|
||||||
@@ -167,11 +168,17 @@ Example::
|
|||||||
``user``, ``password``, ``host``, ``port``
|
``user``, ``password``, ``host``, ``port``
|
||||||
Connection details for the database connection. Empty by default.
|
Connection details for the database connection. Empty by default.
|
||||||
|
|
||||||
|
``advisory_lock_index``
|
||||||
|
On PostgreSQL, pretix uses the "advisory lock" feature. However, advisory locks use a server-wide name space and
|
||||||
|
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
|
||||||
|
you should set separate values for this setting (integers up to 256).
|
||||||
|
|
||||||
``sslmode``, ``sslrootcert``
|
``sslmode``, ``sslrootcert``
|
||||||
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
|
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
|
||||||
|
|
||||||
``sslcert``, ``sslkey``
|
``sslcert``, ``sslkey``
|
||||||
Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default.
|
Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default.
|
||||||
|
|
||||||
.. _`config-replica`:
|
.. _`config-replica`:
|
||||||
|
|
||||||
Database replica settings
|
Database replica settings
|
||||||
|
|||||||
@@ -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
|
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||||
offers at `pretix.eu`_.
|
offers at `pretix.eu`_.
|
||||||
|
|
||||||
We tested this guide on the Linux distribution **Debian 11.6** but it should work very similar on other
|
We tested this guide on the Linux distribution **Debian 12** but it should work very similar on other
|
||||||
modern distributions, especially on all systemd-based ones.
|
modern distributions, especially on all systemd-based ones.
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
@@ -64,7 +64,7 @@ Package dependencies
|
|||||||
|
|
||||||
To build and run pretix, you will need the following debian packages::
|
To build and run pretix, you will need the following debian packages::
|
||||||
|
|
||||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||||
|
|
||||||
@@ -130,9 +130,10 @@ We now install pretix, its direct dependencies and gunicorn::
|
|||||||
|
|
||||||
Note that you need Python 3.9 or newer. You can find out your Python version using ``python -V``.
|
Note that you need Python 3.9 or newer. You can find out your Python version using ``python -V``.
|
||||||
|
|
||||||
We also need to create a data directory::
|
We also need to create a data directory and allow your webserver to traverse to the root directory::
|
||||||
|
|
||||||
(venv)$ mkdir -p /var/pretix/data/media
|
(venv)$ mkdir -p /var/pretix/data/media
|
||||||
|
(venv)$ chmod +x /var/pretix
|
||||||
|
|
||||||
Finally, we compile static files and translation data and create the database structure::
|
Finally, we compile static files and translation data and create the database structure::
|
||||||
|
|
||||||
@@ -248,14 +249,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/pretix/venv/lib/python3.10/site-packages/pretix/static.dist/;
|
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
||||||
access_log off;
|
access_log off;
|
||||||
expires 365d;
|
expires 365d;
|
||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.. note:: Remember to replace the ``python3.10`` in the ``/static/`` path in the config
|
.. note:: Remember to replace the ``python3.11`` in the ``/static/`` path in the config
|
||||||
above with your python version.
|
above with your python version.
|
||||||
|
|
||||||
We recommend reading about setting `strong encryption settings`_ for your web server.
|
We recommend reading about setting `strong encryption settings`_ for your web server.
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ already upgraded to pretix 5.0 or later, downgrade back to the last 4.x release
|
|||||||
Update database schema
|
Update database schema
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Before you start, make sure your database schema is up to date::
|
Before you start, make sure your database schema is up to date. With a local installation::
|
||||||
|
|
||||||
# sudo -u pretix -s
|
# sudo -u pretix -s
|
||||||
$ source /var/pretix/venv/bin/activate
|
$ source /var/pretix/venv/bin/activate
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
|
|
||||||
|
With a docker installation::
|
||||||
|
|
||||||
|
docker exec -it pretix.service pretix migrate
|
||||||
|
|
||||||
|
|
||||||
Install PostgreSQL
|
Install PostgreSQL
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
@@ -70,10 +75,14 @@ Of course, instead of all this you can also run a PostgreSQL docker container an
|
|||||||
Stop pretix
|
Stop pretix
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
To prevent any more changes to your data, stop pretix from running::
|
To prevent any more changes to your data, stop pretix from running. With a local installation::
|
||||||
|
|
||||||
# systemctl stop pretix-web pretix-worker
|
# systemctl stop pretix-web pretix-worker
|
||||||
|
|
||||||
|
With docker::
|
||||||
|
|
||||||
|
# systemctl stop pretix
|
||||||
|
|
||||||
Change configuration
|
Change configuration
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -90,12 +99,16 @@ Change the database configuration in your ``/etc/pretix/pretix.cfg`` file::
|
|||||||
Create database schema
|
Create database schema
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
To create the schema in your new PostgreSQL database, use the following commands::
|
To create the schema in your new PostgreSQL database, use the following commands. With a local installation::
|
||||||
|
|
||||||
# sudo -u pretix -s
|
# sudo -u pretix -s
|
||||||
$ source /var/pretix/venv/bin/activate
|
$ source /var/pretix/venv/bin/activate
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
|
|
||||||
|
With docker::
|
||||||
|
|
||||||
|
# docker run --rm -v /var/pretix-data:/data -v /etc/pretix:/etc/pretix -v /var/run/redis:/var/run/redis pretix/standalone:stable migrate
|
||||||
|
|
||||||
|
|
||||||
Migrate your data
|
Migrate your data
|
||||||
-----------------
|
-----------------
|
||||||
@@ -144,11 +157,18 @@ Afterwards, delete the file again::
|
|||||||
Start pretix
|
Start pretix
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Now, restart pretix. Maybe stop your MySQL server as a verification step that you are no longer using it::
|
Stop your MySQL server as a verification step that you are no longer using it::
|
||||||
|
|
||||||
# systemctl stop mariadb
|
# systemctl stop mariadb
|
||||||
|
|
||||||
|
Then, restart pretix. With a local installation::
|
||||||
|
|
||||||
# systemctl start pretix-web pretix-worker
|
# systemctl start pretix-web pretix-worker
|
||||||
|
|
||||||
|
With a docker installation::
|
||||||
|
|
||||||
|
# systemctl start pretix
|
||||||
|
|
||||||
And you're done! After you've verified everything has been copied correctly, you can delete the old MySQL database.
|
And you're done! After you've verified everything has been copied correctly, you can delete the old MySQL database.
|
||||||
|
|
||||||
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
|
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ last_modified datetime Last modificati
|
|||||||
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
||||||
added.
|
added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.8.0
|
||||||
|
|
||||||
|
For the organizer-wide endpoint, the ``search`` query parameter has been modified to filter sub-events by their parent events slug too.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -472,6 +476,7 @@ Endpoints
|
|||||||
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
|
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
|
||||||
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
|
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
|
||||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||||
|
:query search: Only return events matching a given search query.
|
||||||
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
|
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.live.deactivated``
|
* ``pretix.event.live.deactivated``
|
||||||
* ``pretix.event.testmode.activated``
|
* ``pretix.event.testmode.activated``
|
||||||
* ``pretix.event.testmode.deactivated``
|
* ``pretix.event.testmode.deactivated``
|
||||||
|
* ``pretix.customer.created``
|
||||||
|
* ``pretix.customer.changed``
|
||||||
|
* ``pretix.customer.anonymized``
|
||||||
|
|
||||||
Installed plugins might register more valid values.
|
Installed plugins might register more valid values.
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ Contents:
|
|||||||
email
|
email
|
||||||
permissions
|
permissions
|
||||||
logging
|
logging
|
||||||
|
locking
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
Resource locking
|
||||||
|
================
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.8
|
||||||
|
|
||||||
|
Our locking mechanism changed heavily in version 2023.8. Read `this PR`_ for background information.
|
||||||
|
|
||||||
|
One of pretix's core objectives as a ticketing system could be described as the management of scarce resources.
|
||||||
|
Specifically, the following types of scarce-ness exist in pretix:
|
||||||
|
|
||||||
|
- Quotas can limit the number of tickets available
|
||||||
|
- Seats can only be booked once
|
||||||
|
- Vouchers can only be used a limited number of times
|
||||||
|
- Some memberships can only be used a limited number of times
|
||||||
|
|
||||||
|
For all of these, it is critical that we prevent race conditions.
|
||||||
|
While for some events it wouldn't be a big deal to sell a ticket more or less, for some it would be problematic and selling the same seat twice would always be catastrophic.
|
||||||
|
|
||||||
|
We therefore implement a standardized locking approach across the system to limit concurrency in cases where it could
|
||||||
|
be problematic.
|
||||||
|
|
||||||
|
To acquire a lock on a set of quotas to create a new order that uses that quota, you should follow the following pattern::
|
||||||
|
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
quotas = Quota.objects.filter(...)
|
||||||
|
lock_objects(quotas, shared_lock_objects=[event])
|
||||||
|
check_quota(quotas)
|
||||||
|
create_ticket()
|
||||||
|
|
||||||
|
The lock will automatically be released at the end of your database transaction.
|
||||||
|
|
||||||
|
Generally, follow the following guidelines during your development:
|
||||||
|
|
||||||
|
- **Always** acquire a lock on every **quota**, **voucher** or **seat** that you "use" during your transaction. "Use"
|
||||||
|
here means any action after which the quota, voucher or seat will be **less available**, such as creating a cart
|
||||||
|
position, creating an order, creating a blocking voucher, etc.
|
||||||
|
|
||||||
|
- There is **no need** to acquire a lock if you **free up** capacity, e.g. by canceling an order, deleting a voucher, etc.
|
||||||
|
|
||||||
|
- **Always** acquire a shared lock on the ``event`` you are working in whenever you acquire a lock on a quota, voucher,
|
||||||
|
or seat.
|
||||||
|
|
||||||
|
- Only call ``lock_objects`` **once** per transaction. If you violate this rule, `deadlocks`_ become possible.
|
||||||
|
|
||||||
|
- For best performance, call ``lock_objects`` as **late** in your transaction as possible, but always before you check
|
||||||
|
if the desired resource is still available in sufficient quantity.
|
||||||
|
|
||||||
|
Behind the scenes, the locking is implemented through `PostgreSQL advisory locks`_. You should also be aware of the following
|
||||||
|
properties of our system:
|
||||||
|
|
||||||
|
- In some situations, an exclusive lock on the ``event`` is used, such as when the system can't determine for sure which
|
||||||
|
seats will become unavailable after the transaction.
|
||||||
|
|
||||||
|
- An exclusive lock on the event is also used if you pass more than 20 objects to ``lock_objects``. This is a performance
|
||||||
|
trade-off because it would take long to acquire all of the individual locks.
|
||||||
|
|
||||||
|
- If ``lock_objects`` is unable to acquire a lock within 3 seconds, a ``LockTimeoutException`` will be thrown.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
We currently do not use ``lock_objects`` for memberships. Instead, we use ``select_for_update()`` on the membership
|
||||||
|
model. This might change in the future, but you should usually not be concerned about it since
|
||||||
|
``validate_memberships_in_order(lock=True)`` will handle it for you.
|
||||||
|
|
||||||
|
.. _this PR: https://github.com/pretix/pretix/pull/2408
|
||||||
|
.. _deadlocks: https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS
|
||||||
|
.. _PostgreSQL advisory locks: https://www.postgresql.org/docs/11/explicit-locking.html#ADVISORY-LOCKS
|
||||||
+65
-108
@@ -293,6 +293,16 @@ with that information::
|
|||||||
</pretix-widget>
|
</pretix-widget>
|
||||||
|
|
||||||
This works for the pretix Button as well, if you also specify a product.
|
This works for the pretix Button as well, if you also specify a product.
|
||||||
|
|
||||||
|
As data-attributes are reactive, you can change them with JavaScript as well. Please note, that once the user
|
||||||
|
started the checkout process, we do not update the data-attributes in the existing checkout process to not
|
||||||
|
interrupt the checkout UX.
|
||||||
|
|
||||||
|
When updating data-attributes through JavaScript, make sure you do not have a stale reference to the HTMLNode of the
|
||||||
|
widget. When the widget is created, the original HTMLNode can happen to be replaced. So make sure to always have a
|
||||||
|
fresh reference like so
|
||||||
|
``document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper")``
|
||||||
|
|
||||||
Currently, the following attributes are understood by pretix itself:
|
Currently, the following attributes are understood by pretix itself:
|
||||||
|
|
||||||
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
|
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
|
||||||
@@ -329,125 +339,72 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
|||||||
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
|
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
|
||||||
made through this widget will be counted towards this campaign.
|
made through this widget will be counted towards this campaign.
|
||||||
|
|
||||||
* If you use the tracking plugin, you can enable cross-domain tracking. To do so, you need to initialize the
|
* If you use the tracking plugin, you can enable cross-domain tracking. Please note: when you run your pretix-shop on a
|
||||||
pretix-widget manually. Use the html code to embed the widget and add one the following code snippets. Make sure to
|
subdomain of your main tracking domain, then you do not need cross-domain tracking as tracking automatically works
|
||||||
replace all occurrences of <MEASUREMENT_ID> with your Google Analytics MEASUREMENT_ID (UA-XXXXXXX-X or G-XXXXXXXX)
|
across subdomains. See :ref:`custom_domain` for how to set this up.
|
||||||
|
|
||||||
Please also make sure to add the embedding website to your `Referral exclusions
|
Please make sure to add the embedding website to your `Referral exclusions
|
||||||
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
|
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
|
||||||
|
|
||||||
If you use Google Analytics 4 (GA4 – G-XXXXXXXX)::
|
Add Google Analytics as you normally would with all your `window.dataLayer` and `gtag` configurations. Also add the
|
||||||
|
widget code normally. Then you have two options:
|
||||||
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
* Block loading of the widget at most 2 seconds or until Google’s client- and session-ID are loaded. This method
|
||||||
<script type="text/javascript">
|
uses `window.pretixWidgetCallback`. Note that if it takes longer than 2 seconds to load, client- and session-ID
|
||||||
window.dataLayer = window.dataLayer || [];
|
are never passed to the widget. Make sure to replace all occurrences of <MEASUREMENT_ID> with your Google
|
||||||
function gtag(){dataLayer.push(arguments);}
|
Analytics MEASUREMENT_ID (G-XXXXXXXX)::
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', '<MEASUREMENT_ID>');
|
|
||||||
|
|
||||||
window.pretixWidgetCallback = function () {
|
<script type="text/javascript">
|
||||||
window.PretixWidget.build_widgets = false;
|
window.pretixWidgetCallback = function () {
|
||||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
window.PretixWidget.build_widgets = false;
|
||||||
if (!window['google_tag_manager']) {
|
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||||
window.PretixWidget.buildWidgets();
|
if (!window['google_tag_manager']) {
|
||||||
return;
|
window.PretixWidget.buildWidgets();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var clientId;
|
var clientId;
|
||||||
var sessionId;
|
var sessionId;
|
||||||
var loadingTimeout;
|
var loadingTimeout;
|
||||||
function build() {
|
function build() {
|
||||||
// use loadingTimeout to make sure build() is only called once
|
// use loadingTimeout to make sure build() is only called once
|
||||||
if (!loadingTimeout) return;
|
if (!loadingTimeout) return;
|
||||||
window.clearTimeout(loadingTimeout);
|
window.clearTimeout(loadingTimeout);
|
||||||
loadingTimeout = null;
|
loadingTimeout = null;
|
||||||
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
|
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
|
||||||
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
|
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
|
||||||
window.PretixWidget.buildWidgets();
|
window.PretixWidget.buildWidgets();
|
||||||
};
|
};
|
||||||
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
|
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
|
||||||
loadingTimeout = window.setTimeout(build, 2000);
|
loadingTimeout = window.setTimeout(build, 2000);
|
||||||
|
|
||||||
|
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||||
|
clientId = id;
|
||||||
|
if (sessionId !== undefined) build();
|
||||||
|
});
|
||||||
|
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
|
||||||
|
sessionId = id;
|
||||||
|
if (clientId !== undefined) build();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
* Or asynchronously set data-attributes – the widgets are shown immediately, but once the user has started checkout,
|
||||||
|
data-attributes are not updated. Make sure to replace all occurrences of <MEASUREMENT_ID> with your Google
|
||||||
|
Analytics MEASUREMENT_ID (G-XXXXXXXX)::
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener('load', function() {
|
||||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||||
clientId = id;
|
const widgets = document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper");
|
||||||
if (sessionId !== undefined) build();
|
widgets.forEach(widget => widget.setAttribute("data-tracking-ga-id", id))
|
||||||
});
|
});
|
||||||
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
|
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
|
||||||
sessionId = id;
|
const widgets = document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper");
|
||||||
if (clientId !== undefined) build();
|
widgets.forEach(widget => widget.setAttribute("data-tracking-ga-sessid", id))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
</script>
|
||||||
</script>
|
|
||||||
|
|
||||||
If you use Universal Analytics with ``gtag.js`` (UA-XXXXXXX-X)::
|
|
||||||
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', '<MEASUREMENT_ID>');
|
|
||||||
|
|
||||||
window.pretixWidgetCallback = function () {
|
|
||||||
window.PretixWidget.build_widgets = false;
|
|
||||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
|
||||||
if (!window['google_tag_manager']) {
|
|
||||||
window.PretixWidget.buildWidgets();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to build pretix-widgets if gtag fails to load client_id
|
|
||||||
var loadingTimeout = window.setTimeout(function() {
|
|
||||||
loadingTimeout = null;
|
|
||||||
window.PretixWidget.buildWidgets();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
|
||||||
if (loadingTimeout) {
|
|
||||||
window.clearTimeout(loadingTimeout);
|
|
||||||
window.PretixWidget.widget_data["tracking-ga-id"] = id;
|
|
||||||
window.PretixWidget.buildWidgets();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
If you use ``analytics.js`` (Universal Analytics)::
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
|
||||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
|
||||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
|
||||||
|
|
||||||
ga('create', '<MEASUREMENT_ID>', 'auto');
|
|
||||||
ga('send', 'pageview');
|
|
||||||
|
|
||||||
window.pretixWidgetCallback = function () {
|
|
||||||
window.PretixWidget.build_widgets = false;
|
|
||||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
|
||||||
if (!window['ga'] || !ga.create) {
|
|
||||||
// Tracking is probably blocked
|
|
||||||
window.PretixWidget.buildWidgets()
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadingTimeout = window.setTimeout(function() {
|
|
||||||
loadingTimeout = null;
|
|
||||||
window.PretixWidget.buildWidgets();
|
|
||||||
}, 1000);
|
|
||||||
ga(function(tracker) {
|
|
||||||
if (loadingTimeout) {
|
|
||||||
window.clearTimeout(loadingTimeout);
|
|
||||||
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
|
|
||||||
window.PretixWidget.buildWidgets();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
.. _Let's Encrypt: https://letsencrypt.org/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
|
"aiohttp==3.8.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.18.*",
|
"fakeredis==2.18.*",
|
||||||
@@ -120,6 +121,7 @@ dev = [
|
|||||||
"potypo",
|
"potypo",
|
||||||
"pycodestyle==2.10.*",
|
"pycodestyle==2.10.*",
|
||||||
"pyflakes==3.0.*",
|
"pyflakes==3.0.*",
|
||||||
|
"pytest-asyncio",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ ALL_LANGUAGES = [
|
|||||||
('fi', _('Finnish')),
|
('fi', _('Finnish')),
|
||||||
('gl', _('Galician')),
|
('gl', _('Galician')),
|
||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
|
('id', _('Indonesian')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
('lv', _('Latvian')),
|
('lv', _('Latvian')),
|
||||||
('pl', _('Polish')),
|
('pl', _('Polish')),
|
||||||
@@ -259,3 +260,20 @@ PRETIX_PRIMARY_COLOR = '#8E44B3'
|
|||||||
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
|
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
|
||||||
CACHE_LARGE_VALUES_ALLOWED = False
|
CACHE_LARGE_VALUES_ALLOWED = False
|
||||||
CACHE_LARGE_VALUES_ALIAS = 'default'
|
CACHE_LARGE_VALUES_ALIAS = 'default'
|
||||||
|
|
||||||
|
# Allowed file extensions for various places plus matching Pillow formats.
|
||||||
|
# Never allow EPS, it is full of dangerous bugs.
|
||||||
|
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
||||||
|
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
||||||
|
|
||||||
|
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
||||||
|
|
||||||
|
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||||
|
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
||||||
|
|
||||||
|
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||||
|
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||||
|
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||||
|
".bmp", ".tif", ".tiff"
|
||||||
|
)
|
||||||
|
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||||
|
|||||||
+337
-306
@@ -22,6 +22,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pycountry
|
import pycountry
|
||||||
@@ -59,10 +60,11 @@ from pretix.base.models.orders import (
|
|||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
from pretix.base.services.locking import NoLockManager
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||||
from pretix.base.services.pricing import (
|
from pretix.base.services.pricing import (
|
||||||
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
||||||
)
|
)
|
||||||
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
@@ -1144,338 +1146,367 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
ia = None
|
ia = None
|
||||||
|
|
||||||
lock_required = False
|
quotas_by_item = {}
|
||||||
|
quota_diff_for_locking = Counter()
|
||||||
|
voucher_diff_for_locking = Counter()
|
||||||
|
seat_diff_for_locking = Counter()
|
||||||
|
quota_usage = Counter()
|
||||||
|
voucher_usage = Counter()
|
||||||
|
seat_usage = Counter()
|
||||||
|
v_budget = {}
|
||||||
|
now_dt = now()
|
||||||
|
delete_cps = []
|
||||||
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
|
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
pos_data['_quotas'] = list(
|
if (pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')) not in quotas_by_item:
|
||||||
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')] = list(
|
||||||
if pos_data.get('variation')
|
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||||
else pos_data.get('item').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 pos_data.get('voucher') or pos_data.get('seat') or any(q.size is not None for q in pos_data['_quotas']):
|
)
|
||||||
lock_required = True
|
for q in quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')]:
|
||||||
|
quota_diff_for_locking[q] += 1
|
||||||
|
if pos_data.get('voucher'):
|
||||||
|
voucher_diff_for_locking[pos_data['voucher']] += 1
|
||||||
|
if pos_data.get('seat'):
|
||||||
|
try:
|
||||||
|
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
||||||
|
except Seat.DoesNotExist:
|
||||||
|
pos_data['seat'] = Seat.DoesNotExist
|
||||||
|
else:
|
||||||
|
pos_data['seat'] = seat
|
||||||
|
seat_diff_for_locking[pos_data['seat']] += 1
|
||||||
|
|
||||||
lockfn = self.context['event'].lock
|
if consume_carts:
|
||||||
if simulate or not lock_required:
|
offset = now() + timedelta(seconds=LOCK_TRUST_WINDOW)
|
||||||
lockfn = NoLockManager
|
for cp in CartPosition.objects.filter(
|
||||||
with lockfn() as now_dt:
|
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now_dt
|
||||||
free_seats = set()
|
):
|
||||||
seats_seen = set()
|
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||||
consume_carts = validated_data.pop('consume_carts', [])
|
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||||
delete_cps = []
|
for quota in quotas:
|
||||||
quota_avail_cache = {}
|
if cp.expires > offset:
|
||||||
v_budget = {}
|
quota_diff_for_locking[quota] -= 1
|
||||||
voucher_usage = Counter()
|
quota_usage[quota] -= 1
|
||||||
if consume_carts:
|
if cp.voucher:
|
||||||
for cp in CartPosition.objects.filter(
|
if cp.expires > offset:
|
||||||
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
|
voucher_diff_for_locking[cp.voucher] -= 1
|
||||||
):
|
voucher_usage[cp.voucher] -= 1
|
||||||
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
if cp.seat:
|
||||||
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
if cp.expires > offset:
|
||||||
for quota in quotas:
|
seat_diff_for_locking[cp.seat] -= 1
|
||||||
if quota not in quota_avail_cache:
|
seat_usage[cp.seat] -= 1
|
||||||
quota_avail_cache[quota] = list(quota.availability())
|
delete_cps.append(cp)
|
||||||
if quota_avail_cache[quota][1] is not None:
|
|
||||||
quota_avail_cache[quota][1] += 1
|
|
||||||
if cp.voucher:
|
|
||||||
voucher_usage[cp.voucher] -= 1
|
|
||||||
if cp.expires > now_dt:
|
|
||||||
if cp.seat:
|
|
||||||
free_seats.add(cp.seat)
|
|
||||||
delete_cps.append(cp)
|
|
||||||
|
|
||||||
errs = [{} for p in positions_data]
|
if not simulate:
|
||||||
|
full_lock_required = seat_diff_for_locking and self.context['event'].settings.seating_minimal_distance > 0
|
||||||
|
if full_lock_required:
|
||||||
|
# We lock the entire event in this case since we don't want to deal with fine-granular locking
|
||||||
|
# in the case of seating distance enforcement
|
||||||
|
lock_objects([self.context['event']])
|
||||||
|
else:
|
||||||
|
lock_objects(
|
||||||
|
[q for q, d in quota_diff_for_locking.items() if d > 0 and q.size is not None and not force] +
|
||||||
|
[v for v, d in voucher_diff_for_locking.items() if d > 0 and not force] +
|
||||||
|
[s for s, d in seat_diff_for_locking.items() if d > 0],
|
||||||
|
shared_lock_objects=[self.context['event']]
|
||||||
|
)
|
||||||
|
|
||||||
for i, pos_data in enumerate(positions_data):
|
qa = QuotaAvailability()
|
||||||
|
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
|
||||||
|
qa.compute()
|
||||||
|
|
||||||
if pos_data.get('voucher'):
|
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
|
||||||
v = pos_data['voucher']
|
# use further down
|
||||||
|
del quota_diff_for_locking, voucher_diff_for_locking, seat_diff_for_locking
|
||||||
|
|
||||||
if pos_data.get('addon_to'):
|
errs = [{} for p in positions_data]
|
||||||
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
|
for i, pos_data in enumerate(positions_data):
|
||||||
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
|
if pos_data.get('voucher'):
|
||||||
continue
|
v = pos_data['voucher']
|
||||||
|
|
||||||
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
|
if pos_data.get('addon_to'):
|
||||||
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
|
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if v.valid_until is not None and v.valid_until < now_dt:
|
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
|
||||||
errs[i]['voucher'] = [error_messages['voucher_expired']]
|
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
voucher_usage[v] += 1
|
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
|
||||||
if voucher_usage[v] > 0:
|
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
|
||||||
redeemed_in_carts = CartPosition.objects.filter(
|
continue
|
||||||
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
|
||||||
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
|
||||||
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
|
||||||
if v_avail < voucher_usage[v]:
|
|
||||||
errs[i]['voucher'] = [
|
|
||||||
'The voucher has already been used the maximum number of times.'
|
|
||||||
]
|
|
||||||
|
|
||||||
if v.budget is not None:
|
if v.valid_until is not None and v.valid_until < now_dt:
|
||||||
price = pos_data.get('price')
|
errs[i]['voucher'] = [error_messages['voucher_expired']]
|
||||||
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
|
continue
|
||||||
|
|
||||||
if pos_data.get('voucher'):
|
voucher_usage[v] += 1
|
||||||
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
|
if voucher_usage[v] > 0:
|
||||||
else:
|
redeemed_in_carts = CartPosition.objects.filter(
|
||||||
price_after_voucher = listed_price
|
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
||||||
if price is None:
|
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
||||||
price = price_after_voucher
|
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
||||||
|
if v_avail < voucher_usage[v]:
|
||||||
|
errs[i]['voucher'] = [
|
||||||
|
'The voucher has already been used the maximum number of times.'
|
||||||
|
]
|
||||||
|
|
||||||
if v not in v_budget:
|
if v.budget is not None:
|
||||||
v_budget[v] = v.budget - v.budget_used()
|
price = pos_data.get('price')
|
||||||
disc = max(listed_price - price, 0)
|
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
|
||||||
if disc > v_budget[v]:
|
|
||||||
new_disc = v_budget[v]
|
|
||||||
v_budget[v] -= new_disc
|
|
||||||
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
|
|
||||||
errs[i]['voucher'] = [
|
|
||||||
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
|
|
||||||
'given.'.format(v_budget[v] + new_disc, disc)
|
|
||||||
]
|
|
||||||
continue
|
|
||||||
pos_data['price'] = price + (disc - new_disc)
|
|
||||||
else:
|
|
||||||
v_budget[v] -= disc
|
|
||||||
|
|
||||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
|
||||||
if pos_data.get('seat'):
|
|
||||||
if pos_data.get('addon_to'):
|
|
||||||
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not seated:
|
|
||||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
|
||||||
try:
|
|
||||||
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
|
||||||
except Seat.DoesNotExist:
|
|
||||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
|
||||||
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)]
|
|
||||||
seats_seen.add(seat)
|
|
||||||
elif seated:
|
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
|
||||||
|
|
||||||
requested_valid_from = pos_data.pop('requested_valid_from', None)
|
|
||||||
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
|
||||||
valid_from, valid_until = pos_data['item'].compute_validity(
|
|
||||||
requested_start=(
|
|
||||||
max(requested_valid_from, now())
|
|
||||||
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
|
||||||
else now()
|
|
||||||
),
|
|
||||||
enforce_start_limit=True,
|
|
||||||
override_tz=self.context['event'].timezone,
|
|
||||||
)
|
|
||||||
pos_data['valid_from'] = valid_from
|
|
||||||
pos_data['valid_until'] = valid_until
|
|
||||||
|
|
||||||
if not force:
|
|
||||||
for i, pos_data in enumerate(positions_data):
|
|
||||||
if pos_data.get('voucher'):
|
if pos_data.get('voucher'):
|
||||||
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
|
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
|
||||||
|
else:
|
||||||
|
price_after_voucher = listed_price
|
||||||
|
if price is None:
|
||||||
|
price = price_after_voucher
|
||||||
|
|
||||||
|
if v not in v_budget:
|
||||||
|
v_budget[v] = v.budget - v.budget_used()
|
||||||
|
disc = max(listed_price - price, 0)
|
||||||
|
if disc > v_budget[v]:
|
||||||
|
new_disc = v_budget[v]
|
||||||
|
v_budget[v] -= new_disc
|
||||||
|
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
|
||||||
|
errs[i]['voucher'] = [
|
||||||
|
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
|
||||||
|
'given.'.format(v_budget[v] + new_disc, disc)
|
||||||
|
]
|
||||||
continue
|
continue
|
||||||
|
pos_data['price'] = price + (disc - new_disc)
|
||||||
|
else:
|
||||||
|
v_budget[v] -= disc
|
||||||
|
|
||||||
if pos_data.get('subevent'):
|
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||||
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
|
if pos_data.get('seat'):
|
||||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
|
if pos_data.get('addon_to'):
|
||||||
str(pos_data.get('item'))
|
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
|
||||||
)]
|
continue
|
||||||
if (
|
if not seated:
|
||||||
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
|
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||||
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
|
seat = pos_data['seat']
|
||||||
):
|
if seat is Seat.DoesNotExist:
|
||||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
|
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||||
str(pos_data.get('item'))
|
else:
|
||||||
)]
|
seat_usage[seat] += 1
|
||||||
|
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
||||||
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
|
elif seated:
|
||||||
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
|
|
||||||
new_quotas = pos_data['_quotas']
|
requested_valid_from = pos_data.pop('requested_valid_from', None)
|
||||||
if len(new_quotas) == 0:
|
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
||||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
valid_from, valid_until = pos_data['item'].compute_validity(
|
||||||
|
requested_start=(
|
||||||
|
max(requested_valid_from, now())
|
||||||
|
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
||||||
|
else now()
|
||||||
|
),
|
||||||
|
enforce_start_limit=True,
|
||||||
|
override_tz=self.context['event'].timezone,
|
||||||
|
)
|
||||||
|
pos_data['valid_from'] = valid_from
|
||||||
|
pos_data['valid_until'] = valid_until
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
for i, pos_data in enumerate(positions_data):
|
||||||
|
if pos_data.get('voucher'):
|
||||||
|
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'))
|
str(pos_data.get('item'))
|
||||||
)]
|
)]
|
||||||
else:
|
|
||||||
for quota in new_quotas:
|
|
||||||
if quota not in quota_avail_cache:
|
|
||||||
quota_avail_cache[quota] = list(quota.availability())
|
|
||||||
|
|
||||||
if quota_avail_cache[quota][1] is not None:
|
new_quotas = quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')]
|
||||||
quota_avail_cache[quota][1] -= 1
|
if len(new_quotas) == 0:
|
||||||
if quota_avail_cache[quota][1] < 0:
|
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
errs[i]['item'] = [
|
str(pos_data.get('item'))
|
||||||
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
)]
|
||||||
quota.name
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if any(errs):
|
|
||||||
raise ValidationError({'positions': errs})
|
|
||||||
|
|
||||||
if validated_data.get('locale', None) is None:
|
|
||||||
validated_data['locale'] = self.context['event'].settings.locale
|
|
||||||
order = Order(event=self.context['event'], **validated_data)
|
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
|
||||||
order.meta_info = "{}"
|
|
||||||
order.total = Decimal('0.00')
|
|
||||||
if validated_data.get('require_approval') is not None:
|
|
||||||
order.require_approval = validated_data['require_approval']
|
|
||||||
if simulate:
|
|
||||||
order = WrappedModel(order)
|
|
||||||
order.last_modified = now()
|
|
||||||
order.code = 'PREVIEW'
|
|
||||||
else:
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
if ia:
|
|
||||||
if not simulate:
|
|
||||||
ia.order = order
|
|
||||||
ia.save()
|
|
||||||
else:
|
else:
|
||||||
order.invoice_address = ia
|
for quota in new_quotas:
|
||||||
ia.last_modified = now()
|
quota_usage[quota] += 1
|
||||||
|
if quota_usage[quota] > 0 and qa.results[quota][1] is not None:
|
||||||
|
if qa.results[quota][1] < quota_usage[quota]:
|
||||||
|
errs[i]['item'] = [
|
||||||
|
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||||
|
quota.name
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
# Generate position objects
|
if any(errs):
|
||||||
pos_map = {}
|
raise ValidationError({'positions': errs})
|
||||||
for pos_data in positions_data:
|
|
||||||
addon_to = pos_data.pop('addon_to', None)
|
|
||||||
attendee_name = pos_data.pop('attendee_name', '')
|
|
||||||
if attendee_name and not pos_data.get('attendee_name_parts'):
|
|
||||||
pos_data['attendee_name_parts'] = {
|
|
||||||
'_legacy': attendee_name
|
|
||||||
}
|
|
||||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
|
|
||||||
if simulate:
|
|
||||||
pos.order = order._wrapped
|
|
||||||
else:
|
|
||||||
pos.order = order
|
|
||||||
if addon_to:
|
|
||||||
if simulate:
|
|
||||||
pos.addon_to = pos_map[addon_to]
|
|
||||||
else:
|
|
||||||
pos.addon_to = pos_map[addon_to]
|
|
||||||
|
|
||||||
pos_map[pos.positionid] = pos
|
if validated_data.get('locale', None) is None:
|
||||||
pos_data['__instance'] = pos
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
# Calculate prices if not set
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
for pos_data in positions_data:
|
order.meta_info = "{}"
|
||||||
pos = pos_data['__instance']
|
order.total = Decimal('0.00')
|
||||||
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
|
if validated_data.get('require_approval') is not None:
|
||||||
listed_price = Decimal('0.00')
|
order.require_approval = validated_data['require_approval']
|
||||||
else:
|
if simulate:
|
||||||
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
|
order = WrappedModel(order)
|
||||||
|
order.last_modified = now()
|
||||||
if pos.price is None:
|
order.code = 'PREVIEW'
|
||||||
if pos.voucher:
|
else:
|
||||||
price_after_voucher = pos.voucher.calculate_price(listed_price)
|
order.save()
|
||||||
else:
|
|
||||||
price_after_voucher = listed_price
|
|
||||||
|
|
||||||
line_price = get_line_price(
|
|
||||||
price_after_voucher=price_after_voucher,
|
|
||||||
custom_price_input=None,
|
|
||||||
custom_price_input_is_net=False,
|
|
||||||
tax_rule=pos.item.tax_rule,
|
|
||||||
invoice_address=ia,
|
|
||||||
bundled_sum=Decimal('0.00'),
|
|
||||||
)
|
|
||||||
pos.price = line_price.gross
|
|
||||||
pos._auto_generated_price = True
|
|
||||||
else:
|
|
||||||
if pos.voucher:
|
|
||||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
|
||||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
|
||||||
else:
|
|
||||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
|
||||||
else:
|
|
||||||
price_after_voucher = listed_price
|
|
||||||
pos._auto_generated_price = False
|
|
||||||
pos._voucher_discount = listed_price - price_after_voucher
|
|
||||||
if pos.voucher:
|
|
||||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
|
||||||
|
|
||||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
|
||||||
discount_results = apply_discounts(
|
|
||||||
self.context['event'],
|
|
||||||
order.sales_channel,
|
|
||||||
[
|
|
||||||
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
|
||||||
for cp in order_positions
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
|
||||||
if new_price != pos.price and pos._auto_generated_price:
|
|
||||||
pos.price = new_price
|
|
||||||
pos.discount = discount
|
|
||||||
|
|
||||||
# Save instances
|
|
||||||
for pos_data in positions_data:
|
|
||||||
answers_data = pos_data.pop('answers', [])
|
|
||||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
|
||||||
pos = pos_data['__instance']
|
|
||||||
pos._calculate_tax()
|
|
||||||
|
|
||||||
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"
|
|
||||||
pos.checkins = []
|
|
||||||
pos_map[pos.positionid] = pos
|
|
||||||
else:
|
|
||||||
if pos.voucher:
|
|
||||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
|
||||||
pos.save()
|
|
||||||
seen_answers = set()
|
|
||||||
for answ_data in answers_data:
|
|
||||||
# Workaround for a pretixPOS bug :-(
|
|
||||||
if answ_data.get('question') in seen_answers:
|
|
||||||
continue
|
|
||||||
seen_answers.add(answ_data.get('question'))
|
|
||||||
|
|
||||||
options = answ_data.pop('options', [])
|
|
||||||
|
|
||||||
if isinstance(answ_data['answer'], File):
|
|
||||||
an = answ_data.pop('answer')
|
|
||||||
answ = pos.answers.create(**answ_data, answer='')
|
|
||||||
answ.file.save(os.path.basename(an.name), an, save=False)
|
|
||||||
answ.answer = 'file://' + answ.file.name
|
|
||||||
answ.save()
|
|
||||||
else:
|
|
||||||
answ = pos.answers.create(**answ_data)
|
|
||||||
answ.options.add(*options)
|
|
||||||
|
|
||||||
if use_reusable_medium:
|
|
||||||
use_reusable_medium.linked_orderposition = pos
|
|
||||||
use_reusable_medium.save(update_fields=['linked_orderposition'])
|
|
||||||
use_reusable_medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.changed',
|
|
||||||
data={
|
|
||||||
'by_order': order.code,
|
|
||||||
'linked_orderposition': pos.pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if ia:
|
||||||
if not simulate:
|
if not simulate:
|
||||||
for cp in delete_cps:
|
ia.order = order
|
||||||
if cp.addon_to_id:
|
ia.save()
|
||||||
|
else:
|
||||||
|
order.invoice_address = ia
|
||||||
|
ia.last_modified = now()
|
||||||
|
|
||||||
|
# Generate position objects
|
||||||
|
pos_map = {}
|
||||||
|
for pos_data in positions_data:
|
||||||
|
addon_to = pos_data.pop('addon_to', None)
|
||||||
|
attendee_name = pos_data.pop('attendee_name', '')
|
||||||
|
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||||
|
pos_data['attendee_name_parts'] = {
|
||||||
|
'_legacy': attendee_name
|
||||||
|
}
|
||||||
|
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
|
||||||
|
if simulate:
|
||||||
|
pos.order = order._wrapped
|
||||||
|
else:
|
||||||
|
pos.order = order
|
||||||
|
if addon_to:
|
||||||
|
if simulate:
|
||||||
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
else:
|
||||||
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
|
||||||
|
pos_map[pos.positionid] = pos
|
||||||
|
pos_data['__instance'] = pos
|
||||||
|
|
||||||
|
# Calculate prices if not set
|
||||||
|
for pos_data in positions_data:
|
||||||
|
pos = pos_data['__instance']
|
||||||
|
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
|
||||||
|
listed_price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
|
||||||
|
|
||||||
|
if pos.price is None:
|
||||||
|
if pos.voucher:
|
||||||
|
price_after_voucher = pos.voucher.calculate_price(listed_price)
|
||||||
|
else:
|
||||||
|
price_after_voucher = listed_price
|
||||||
|
|
||||||
|
line_price = get_line_price(
|
||||||
|
price_after_voucher=price_after_voucher,
|
||||||
|
custom_price_input=None,
|
||||||
|
custom_price_input_is_net=False,
|
||||||
|
tax_rule=pos.item.tax_rule,
|
||||||
|
invoice_address=ia,
|
||||||
|
bundled_sum=Decimal('0.00'),
|
||||||
|
)
|
||||||
|
pos.price = line_price.gross
|
||||||
|
pos._auto_generated_price = True
|
||||||
|
else:
|
||||||
|
if pos.voucher:
|
||||||
|
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||||
|
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||||
|
else:
|
||||||
|
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||||
|
else:
|
||||||
|
price_after_voucher = listed_price
|
||||||
|
pos._auto_generated_price = False
|
||||||
|
pos._voucher_discount = listed_price - price_after_voucher
|
||||||
|
if pos.voucher:
|
||||||
|
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||||
|
|
||||||
|
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||||
|
discount_results = apply_discounts(
|
||||||
|
self.context['event'],
|
||||||
|
order.sales_channel,
|
||||||
|
[
|
||||||
|
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||||
|
for cp in order_positions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||||
|
if new_price != pos.price and pos._auto_generated_price:
|
||||||
|
pos.price = new_price
|
||||||
|
pos.discount = discount
|
||||||
|
|
||||||
|
# Save instances
|
||||||
|
for pos_data in positions_data:
|
||||||
|
answers_data = pos_data.pop('answers', [])
|
||||||
|
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||||
|
pos = pos_data['__instance']
|
||||||
|
pos._calculate_tax()
|
||||||
|
|
||||||
|
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"
|
||||||
|
pos.checkins = []
|
||||||
|
pos_map[pos.positionid] = pos
|
||||||
|
else:
|
||||||
|
if pos.voucher:
|
||||||
|
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||||
|
pos.save()
|
||||||
|
seen_answers = set()
|
||||||
|
for answ_data in answers_data:
|
||||||
|
# Workaround for a pretixPOS bug :-(
|
||||||
|
if answ_data.get('question') in seen_answers:
|
||||||
continue
|
continue
|
||||||
cp.addons.all().delete()
|
seen_answers.add(answ_data.get('question'))
|
||||||
cp.delete()
|
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
|
||||||
|
if isinstance(answ_data['answer'], File):
|
||||||
|
an = answ_data.pop('answer')
|
||||||
|
answ = pos.answers.create(**answ_data, answer='')
|
||||||
|
answ.file.save(os.path.basename(an.name), an, save=False)
|
||||||
|
answ.answer = 'file://' + answ.file.name
|
||||||
|
answ.save()
|
||||||
|
else:
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
|
||||||
|
if use_reusable_medium:
|
||||||
|
use_reusable_medium.linked_orderposition = pos
|
||||||
|
use_reusable_medium.save(update_fields=['linked_orderposition'])
|
||||||
|
use_reusable_medium.log_action(
|
||||||
|
'pretix.reusable_medium.linked_orderposition.changed',
|
||||||
|
data={
|
||||||
|
'by_order': order.code,
|
||||||
|
'linked_orderposition': pos.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not simulate:
|
||||||
|
for cp in delete_cps:
|
||||||
|
if cp.addon_to_id:
|
||||||
|
continue
|
||||||
|
cp.addons.all().delete()
|
||||||
|
cp.delete()
|
||||||
|
|
||||||
order.total = sum([p.price for p in pos_map.values()])
|
order.total = sum([p.price for p in pos_map.values()])
|
||||||
fees = []
|
fees = []
|
||||||
|
|||||||
@@ -94,8 +94,13 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
if check_quota:
|
if check_quota:
|
||||||
Voucher.clean_quota_check(
|
Voucher.clean_quota_check(
|
||||||
full_data, 1, self.instance, self.context.get('event'),
|
full_data,
|
||||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
|
full_data.get('max_usages', 1) - (self.instance.redeemed if self.instance else 0),
|
||||||
|
self.instance,
|
||||||
|
self.context.get('event'),
|
||||||
|
full_data.get('quota'),
|
||||||
|
full_data.get('item'),
|
||||||
|
full_data.get('variation')
|
||||||
)
|
)
|
||||||
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
|
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from typing import List
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@@ -41,7 +42,7 @@ from pretix.base.models import CartPosition
|
|||||||
from pretix.base.services.cart import (
|
from pretix.base.services.cart import (
|
||||||
_get_quota_availability, _get_voucher_availability, error_messages,
|
_get_quota_availability, _get_voucher_availability, error_messages,
|
||||||
)
|
)
|
||||||
from pretix.base.services.locking import NoLockManager
|
from pretix.base.services.locking import lock_objects
|
||||||
|
|
||||||
|
|
||||||
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
@@ -150,12 +151,21 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
quota_diff[q] += 1
|
quota_diff[q] += 1
|
||||||
|
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
|
now_dt = now()
|
||||||
|
with transaction.atomic():
|
||||||
|
full_lock_required = seat_diff and self.request.event.settings.seating_minimal_distance > 0
|
||||||
|
if full_lock_required:
|
||||||
|
# We lock the entire event in this case since we don't want to deal with fine-granular locking
|
||||||
|
# in the case of seating distance enforcement
|
||||||
|
lock_objects([self.request.event])
|
||||||
|
else:
|
||||||
|
lock_objects(
|
||||||
|
[q for q, d in quota_diff.items() if q.size is not None and d > 0] +
|
||||||
|
[v for v, d in voucher_use_diff.items() if d > 0] +
|
||||||
|
[s for s, d in seat_diff.items() if d > 0],
|
||||||
|
shared_lock_objects=[self.request.event]
|
||||||
|
)
|
||||||
|
|
||||||
lockfn = NoLockManager
|
|
||||||
if self._require_locking(quota_diff, voucher_use_diff, seat_diff):
|
|
||||||
lockfn = self.request.event.lock
|
|
||||||
|
|
||||||
with lockfn() as now_dt, transaction.atomic():
|
|
||||||
vouchers_ok, vouchers_depend_on_cart = _get_voucher_availability(
|
vouchers_ok, vouchers_depend_on_cart = _get_voucher_availability(
|
||||||
self.request.event,
|
self.request.event,
|
||||||
voucher_use_diff,
|
voucher_use_diff,
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.checkinlist = kwargs.pop('checkinlist')
|
self.checkinlist = kwargs.pop('checkinlist')
|
||||||
|
self.gate = kwargs.pop('gate')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
@@ -287,7 +288,7 @@ with scopes_disabled():
|
|||||||
if not self.checkinlist.rules:
|
if not self.checkinlist.rules:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
SQLLogic(self.checkinlist).apply(self.checkinlist.rules)
|
SQLLogic(self.checkinlist, self.gate).apply(self.checkinlist.rules)
|
||||||
).filter(
|
).filter(
|
||||||
Q(valid_from__isnull=True) | Q(valid_from__lte=now()),
|
Q(valid_from__isnull=True) | Q(valid_from__lte=now()),
|
||||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||||
@@ -409,7 +410,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
|
|
||||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||||
source_type='barcode', legacy_url_support=False, simulate=False):
|
source_type='barcode', legacy_url_support=False, simulate=False, gate=None):
|
||||||
if not checkinlists:
|
if not checkinlists:
|
||||||
raise ValidationError('No check-in list passed.')
|
raise ValidationError('No check-in list passed.')
|
||||||
|
|
||||||
@@ -417,7 +418,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
|
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
|
||||||
|
|
||||||
device = auth if isinstance(auth, Device) else None
|
device = auth if isinstance(auth, Device) else None
|
||||||
gate = auth.gate if isinstance(auth, Device) else None
|
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
@@ -672,6 +673,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
raw_source_type=source_type,
|
raw_source_type=source_type,
|
||||||
from_revoked_secret=from_revoked_secret,
|
from_revoked_secret=from_revoked_secret,
|
||||||
simulate=simulate,
|
simulate=simulate,
|
||||||
|
gate=gate,
|
||||||
)
|
)
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
@@ -770,6 +772,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_filterset_kwargs(self):
|
def get_filterset_kwargs(self):
|
||||||
return {
|
return {
|
||||||
'checkinlist': self.checkinlist,
|
'checkinlist': self.checkinlist,
|
||||||
|
'gate': self.request.auth.gate if isinstance(self.request.auth, Device) else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|||||||
@@ -381,16 +381,29 @@ with scopes_disabled():
|
|||||||
| Q(location__icontains=i18ncomp(value))
|
| Q(location__icontains=i18ncomp(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class OrganizerSubEventFilter(SubEventFilter):
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=i18ncomp(value))
|
||||||
|
| Q(event__slug__icontains=value)
|
||||||
|
| Q(location__icontains=i18ncomp(value))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = SubEvent.objects.none()
|
queryset = SubEvent.objects.none()
|
||||||
write_permission = 'can_change_event_settings'
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
filterset_class = SubEventFilter
|
|
||||||
ordering = ('date_from',)
|
ordering = ('date_from',)
|
||||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filterset_class(self):
|
||||||
|
if getattr(self.request, 'event', None):
|
||||||
|
return SubEventFilter
|
||||||
|
return OrganizerSubEventFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if getattr(self.request, 'event', None):
|
if getattr(self.request, 'event', None):
|
||||||
qs = self.request.event.subevents
|
qs = self.request.event.subevents
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from decimal import Decimal
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||||
@@ -116,12 +117,16 @@ with scopes_disabled():
|
|||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def subevent_after_qs(self, qs, name, value):
|
def subevent_after_qs(self, qs, name, value):
|
||||||
|
if getattr(self.request, 'event', None):
|
||||||
|
subevents = self.request.event.subevents
|
||||||
|
else:
|
||||||
|
subevents = SubEvent.objects.filter(event__organizer=self.request.organizer)
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
pk__in=Subquery(
|
pk__in=Subquery(
|
||||||
OrderPosition.all.filter(
|
OrderPosition.all.filter(
|
||||||
subevent_id__in=SubEvent.objects.filter(
|
subevent_id__in=subevents.filter(
|
||||||
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True),
|
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True),
|
||||||
event=self.request.event
|
|
||||||
).values_list('id'),
|
).values_list('id'),
|
||||||
).values_list('order_id')
|
).values_list('order_id')
|
||||||
)
|
)
|
||||||
@@ -129,12 +134,16 @@ with scopes_disabled():
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
def subevent_before_qs(self, qs, name, value):
|
def subevent_before_qs(self, qs, name, value):
|
||||||
|
if getattr(self.request, 'event', None):
|
||||||
|
subevents = self.request.event.subevents
|
||||||
|
else:
|
||||||
|
subevents = SubEvent.objects.filter(event__organizer=self.request.organizer)
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
pk__in=Subquery(
|
pk__in=Subquery(
|
||||||
OrderPosition.all.filter(
|
OrderPosition.all.filter(
|
||||||
subevent_id__in=SubEvent.objects.filter(
|
subevent_id__in=subevents.filter(
|
||||||
Q(date_from__lt=value),
|
Q(date_from__lt=value),
|
||||||
event=self.request.event
|
|
||||||
).values_list('id'),
|
).values_list('id'),
|
||||||
).values_list('order_id')
|
).values_list('order_id')
|
||||||
)
|
)
|
||||||
@@ -290,12 +299,12 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
return Order.objects.filter(
|
return Order.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.auth.get_events_with_permission(perm)
|
event__in=self.request.auth.get_events_with_permission(perm, request=self.request)
|
||||||
)
|
)
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
return Order.objects.filter(
|
return Order.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.user.get_events_with_permission(perm)
|
event__in=self.request.user.get_events_with_permission(perm, request=self.request)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
@@ -1224,7 +1233,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
ftype, ignored = mimetypes.guess_type(image_file.name)
|
ftype, ignored = mimetypes.guess_type(image_file.name)
|
||||||
extension = os.path.basename(image_file.name).split('.')[-1]
|
extension = os.path.basename(image_file.name).split('.')[-1]
|
||||||
else:
|
else:
|
||||||
img = Image.open(image_file)
|
img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||||
ftype = Image.MIME[img.format]
|
ftype = Image.MIME[img.format]
|
||||||
extensions = {
|
extensions = {
|
||||||
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
||||||
@@ -1821,12 +1830,12 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
|
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
qs = Invoice.objects.filter(
|
qs = Invoice.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.auth.get_events_with_permission(perm)
|
event__in=self.request.auth.get_events_with_permission(perm, request=self.request)
|
||||||
)
|
)
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
qs = Invoice.objects.filter(
|
qs = Invoice.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.user.get_events_with_permission(perm)
|
event__in=self.request.user.get_events_with_permission(perm, request=self.request)
|
||||||
)
|
)
|
||||||
return qs.prefetch_related('lines').select_related('order', 'refers').annotate(
|
return qs.prefetch_related('lines').select_related('order', 'refers').annotate(
|
||||||
nr=Concat('prefix', 'invoice_no')
|
nr=Concat('prefix', 'invoice_no')
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import contextlib
|
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@@ -69,30 +67,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.vouchers.select_related('seat').all()
|
return self.request.event.vouchers.select_related('seat').all()
|
||||||
|
|
||||||
def _predict_quota_check(self, data, instance):
|
@transaction.atomic()
|
||||||
# This method predicts if Voucher.clean_quota_needs_checking
|
|
||||||
# *migh* later require a quota check. It is only approximate
|
|
||||||
# and returns True a little too often. The point is to avoid
|
|
||||||
# locks when we know we won't need them.
|
|
||||||
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
|
|
||||||
return False
|
|
||||||
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if 'block_quota' in data and not data.get('block_quota'):
|
|
||||||
return False
|
|
||||||
if instance and 'block_quota' not in data and not instance.block_quota:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
if self._predict_quota_check(request.data, None):
|
return super().create(request, *args, **kwargs)
|
||||||
lockfn = request.event.lock
|
|
||||||
else:
|
|
||||||
lockfn = contextlib.suppress # noop context manager
|
|
||||||
with lockfn():
|
|
||||||
return super().create(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
@@ -108,13 +85,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
if self._predict_quota_check(request.data, self.get_object()):
|
return super().update(request, *args, **kwargs)
|
||||||
lockfn = request.event.lock
|
|
||||||
else:
|
|
||||||
lockfn = contextlib.suppress # noop context manager
|
|
||||||
with lockfn():
|
|
||||||
return super().update(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
@@ -140,22 +113,18 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@action(detail=False, methods=['POST'])
|
@action(detail=False, methods=['POST'])
|
||||||
|
@transaction.atomic()
|
||||||
def batch_create(self, request, *args, **kwargs):
|
def batch_create(self, request, *args, **kwargs):
|
||||||
if any(self._predict_quota_check(d, None) for d in request.data):
|
serializer = self.get_serializer(data=request.data, many=True)
|
||||||
lockfn = request.event.lock
|
serializer.is_valid(raise_exception=True)
|
||||||
else:
|
with transaction.atomic():
|
||||||
lockfn = contextlib.suppress # noop context manager
|
serializer.save(event=self.request.event)
|
||||||
with lockfn():
|
for i, v in enumerate(serializer.instance):
|
||||||
serializer = self.get_serializer(data=request.data, many=True)
|
v.log_action(
|
||||||
serializer.is_valid(raise_exception=True)
|
'pretix.voucher.added',
|
||||||
with transaction.atomic():
|
user=self.request.user,
|
||||||
serializer.save(event=self.request.event)
|
auth=self.request.auth,
|
||||||
for i, v in enumerate(serializer.instance):
|
data=self.request.data[i]
|
||||||
v.log_action(
|
)
|
||||||
'pretix.voucher.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=self.request.data[i]
|
|
||||||
)
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|||||||
@@ -202,6 +202,21 @@ class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ParametrizedCustomerWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
|
def build_payload(self, logentry: LogEntry):
|
||||||
|
customer = logentry.content_object
|
||||||
|
if not customer:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'notification_id': logentry.pk,
|
||||||
|
'organizer': customer.organizer.slug,
|
||||||
|
'customer': customer.identifier,
|
||||||
|
'action': logentry.action_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
|
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
|
||||||
def register_default_webhook_events(sender, **kwargs):
|
def register_default_webhook_events(sender, **kwargs):
|
||||||
return (
|
return (
|
||||||
@@ -350,6 +365,18 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.orders.waitinglist.voucher_assigned',
|
'pretix.event.orders.waitinglist.voucher_assigned',
|
||||||
_('Waiting list entry received voucher'),
|
_('Waiting list entry received voucher'),
|
||||||
),
|
),
|
||||||
|
ParametrizedCustomerWebhookEvent(
|
||||||
|
'pretix.customer.created',
|
||||||
|
_('Customer account created'),
|
||||||
|
),
|
||||||
|
ParametrizedCustomerWebhookEvent(
|
||||||
|
'pretix.customer.changed',
|
||||||
|
_('Customer account changed'),
|
||||||
|
),
|
||||||
|
ParametrizedCustomerWebhookEvent(
|
||||||
|
'pretix.customer.anonymized',
|
||||||
|
_('Customer account anonymized'),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -669,6 +669,11 @@ def base_placeholders(sender, **kwargs):
|
|||||||
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
||||||
_("Mr Doe"),
|
_("Mr Doe"),
|
||||||
))
|
))
|
||||||
|
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||||
|
"name", ["waiting_list_entry"],
|
||||||
|
lambda waiting_list_entry: waiting_list_entry.name or "",
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||||
"name_for_salutation", ["position_or_address"],
|
"name_for_salutation", ["position_or_address"],
|
||||||
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
||||||
|
|||||||
@@ -500,14 +500,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
|||||||
file = BytesIO(data['content'])
|
file = BytesIO(data['content'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(file)
|
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||||
# verify() must be called immediately after the constructor.
|
# verify() must be called immediately after the constructor.
|
||||||
image.verify()
|
image.verify()
|
||||||
|
|
||||||
# We want to do more than just verify(), so we need to re-open the file
|
# We want to do more than just verify(), so we need to re-open the file
|
||||||
if hasattr(file, 'seek'):
|
if hasattr(file, 'seek'):
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
image = Image.open(file)
|
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||||
|
|
||||||
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
||||||
if image.width > 10_000 or image.height > 10_000:
|
if image.width > 10_000 or image.height > 10_000:
|
||||||
@@ -566,7 +566,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
|
kwargs.setdefault('ext_whitelist', settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
||||||
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -826,11 +826,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
initial=initial.file if initial else None,
|
initial=initial.file if initial else None,
|
||||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||||
ext_whitelist=(
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
|
||||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
|
||||||
".bmp", ".tif", ".tiff"
|
|
||||||
),
|
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_DATE:
|
elif q.type == Question.TYPE_DATE:
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ def replace_arabic_numbers(inp):
|
|||||||
return inp.translate(table)
|
return inp.translate(table)
|
||||||
|
|
||||||
|
|
||||||
|
def format_placeholders_help_text(placeholders, event=None):
|
||||||
|
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||||
|
placeholders.sort(key=lambda x: x[0])
|
||||||
|
phs = [
|
||||||
|
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
|
||||||
|
for k, v in placeholders
|
||||||
|
]
|
||||||
|
return _('Available placeholders: {list}').format(
|
||||||
|
list=' '.join(phs)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatePickerWidget(forms.DateInput):
|
class DatePickerWidget(forms.DateInput):
|
||||||
def __init__(self, attrs=None, date_format=None):
|
def __init__(self, attrs=None, date_format=None):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ def metric_values():
|
|||||||
|
|
||||||
# Metrics from redis
|
# Metrics from redis
|
||||||
if settings.HAS_REDIS:
|
if settings.HAS_REDIS:
|
||||||
for key, value in redis.hscan_iter(REDIS_KEY):
|
for key, value in redis.hscan_iter(REDIS_KEY, count=1000):
|
||||||
dkey = key.decode("utf-8")
|
dkey = key.decode("utf-8")
|
||||||
splitted = dkey.split("{", 2)
|
splitted = dkey.split("{", 2)
|
||||||
value = float(value.decode("utf-8"))
|
value = float(value.decode("utf-8"))
|
||||||
|
|||||||
@@ -265,16 +265,16 @@ class CheckinList(LoggedModel):
|
|||||||
# * in pretix.helpers.jsonlogic_boolalg
|
# * in pretix.helpers.jsonlogic_boolalg
|
||||||
# * in checkinrules.js
|
# * in checkinrules.js
|
||||||
# * in libpretixsync
|
# * in libpretixsync
|
||||||
# * in pretixscan-ios (in the future)
|
# * in pretixscan-ios
|
||||||
top_level_operators = {
|
top_level_operators = {
|
||||||
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
||||||
}
|
}
|
||||||
allowed_operators = top_level_operators | {
|
allowed_operators = top_level_operators | {
|
||||||
'buildTime', 'objectList', 'lookup', 'var',
|
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
|
||||||
}
|
}
|
||||||
allowed_vars = {
|
allowed_vars = {
|
||||||
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
||||||
'minutes_since_last_entry', 'minutes_since_first_entry',
|
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
|
||||||
}
|
}
|
||||||
if not rules or not isinstance(rules, dict):
|
if not rules or not isinstance(rules, dict):
|
||||||
return rules
|
return rules
|
||||||
@@ -299,6 +299,10 @@ class CheckinList(LoggedModel):
|
|||||||
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
if operator in ('entries_since', 'entries_before'):
|
||||||
|
if len(values) != 1 or "buildTime" not in values[0]:
|
||||||
|
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
||||||
|
|
||||||
if operator in ('or', 'and') and seen_nonbool:
|
if operator in ('or', 'and') and seen_nonbool:
|
||||||
raise ValidationError('You cannot use OR/AND logic on a level below a comparison operator.')
|
raise ValidationError('You cannot use OR/AND logic on a level below a comparison operator.')
|
||||||
|
|
||||||
|
|||||||
@@ -743,12 +743,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
return ObjectRelatedCache(self)
|
return ObjectRelatedCache(self)
|
||||||
|
|
||||||
def lock(self):
|
def lock(self):
|
||||||
"""
|
raise NotImplementedError("this method has been removed")
|
||||||
Returns a contextmanager that can be used to lock an event for bookings.
|
|
||||||
"""
|
|
||||||
from pretix.base.services import locking
|
|
||||||
|
|
||||||
return locking.LockManager(self)
|
|
||||||
|
|
||||||
def get_mail_backend(self, timeout=None):
|
def get_mail_backend(self, timeout=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -37,10 +37,13 @@ import copy
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
import string
|
import string
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from functools import reduce
|
||||||
|
from time import sleep
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -75,7 +78,6 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Customer, User
|
from pretix.base.models import Customer, User
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
|
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import order_gracefully_delete
|
from pretix.base.signals import order_gracefully_delete
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ from ...helpers import OF_SELF
|
|||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
from ...helpers.format import format_map
|
from ...helpers.format import format_map
|
||||||
from ...helpers.names import build_name
|
from ...helpers.names import build_name
|
||||||
|
from ...testutils.middleware import debugflags_var
|
||||||
from ._transactions import (
|
from ._transactions import (
|
||||||
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
||||||
)
|
)
|
||||||
@@ -825,7 +828,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
if cp.has_checkin:
|
if cp.has_checkin:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.event.settings.get('invoice_address_asked', as_type=bool):
|
if self.event.settings.get('invoice_address_asked', as_type=bool) or self.event.settings.get('invoice_name_required', as_type=bool):
|
||||||
return True
|
return True
|
||||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||||
for cp in positions:
|
for cp in positions:
|
||||||
@@ -923,7 +926,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
else:
|
else:
|
||||||
return expires
|
return expires
|
||||||
|
|
||||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False, lock=False) -> Union[bool, str]:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||||
"payment settings is over."),
|
"payment settings is over."),
|
||||||
@@ -944,10 +947,11 @@ class Order(LockModel, LoggedModel):
|
|||||||
if not self.event.settings.get('payment_term_accept_late') and not ignore_date and not force:
|
if not self.event.settings.get('payment_term_accept_late') and not ignore_date and not force:
|
||||||
return error_messages['late']
|
return error_messages['late']
|
||||||
|
|
||||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
return self._is_still_available(count_waitinglist=count_waitinglist, force=force, lock=lock)
|
||||||
|
|
||||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, lock=False, force=False,
|
||||||
check_voucher_usage=False, check_memberships=False) -> Union[bool, str]:
|
check_voucher_usage=False, check_memberships=False) -> Union[bool, str]:
|
||||||
|
from pretix.base.services.locking import lock_objects
|
||||||
from pretix.base.services.memberships import (
|
from pretix.base.services.memberships import (
|
||||||
validate_memberships_in_order,
|
validate_memberships_in_order,
|
||||||
)
|
)
|
||||||
@@ -966,10 +970,21 @@ class Order(LockModel, LoggedModel):
|
|||||||
try:
|
try:
|
||||||
if check_memberships:
|
if check_memberships:
|
||||||
try:
|
try:
|
||||||
validate_memberships_in_order(self.customer, positions, self.event, lock=False, testmode=self.testmode)
|
validate_memberships_in_order(self.customer, positions, self.event, lock=lock, testmode=self.testmode)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise Quota.QuotaExceededException(e.message)
|
raise Quota.QuotaExceededException(e.message)
|
||||||
|
|
||||||
|
for cp in positions:
|
||||||
|
cp._cached_quotas = list(cp.quotas) if not force else []
|
||||||
|
|
||||||
|
if lock:
|
||||||
|
lock_objects(
|
||||||
|
[q for q in reduce(operator.or_, (set(cp._cached_quotas) for cp in positions), set()) if q.size is not None] +
|
||||||
|
[op.voucher for op in positions if op.voucher and not force] +
|
||||||
|
[op.seat for op in positions if op.seat],
|
||||||
|
shared_lock_objects=[self.event]
|
||||||
|
)
|
||||||
|
|
||||||
for i, op in enumerate(positions):
|
for i, op in enumerate(positions):
|
||||||
if op.seat:
|
if op.seat:
|
||||||
if not op.seat.is_available(ignore_orderpos=op):
|
if not op.seat.is_available(ignore_orderpos=op):
|
||||||
@@ -994,7 +1009,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
voucher=op.voucher.code
|
voucher=op.voucher.code
|
||||||
))
|
))
|
||||||
|
|
||||||
quotas = list(op.quotas)
|
quotas = op._cached_quotas
|
||||||
if len(quotas) == 0:
|
if len(quotas) == 0:
|
||||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||||
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
|
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
|
||||||
@@ -1016,6 +1031,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
))
|
))
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
|
sleep(2)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
||||||
@@ -1246,7 +1264,7 @@ class QuestionAnswer(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_image(self):
|
def is_image(self):
|
||||||
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
|
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
@@ -1647,9 +1665,10 @@ class OrderPayment(models.Model):
|
|||||||
return self.order.event.get_payment_providers(cached=True).get(self.provider)
|
return self.order.event.get_payment_providers(cached=True).get(self.provider)
|
||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
|
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False, lock=False):
|
||||||
from pretix.base.signals import order_paid
|
from pretix.base.signals import order_paid
|
||||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
|
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force,
|
||||||
|
lock=lock)
|
||||||
if can_be_paid is not True:
|
if can_be_paid is not True:
|
||||||
self.order.log_action('pretix.event.order.quotaexceeded', {
|
self.order.log_action('pretix.event.order.quotaexceeded', {
|
||||||
'message': can_be_paid
|
'message': can_be_paid
|
||||||
@@ -1780,25 +1799,24 @@ class OrderPayment(models.Model):
|
|||||||
))
|
))
|
||||||
return
|
return
|
||||||
|
|
||||||
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
|
with transaction.atomic():
|
||||||
generate_invoice)
|
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
|
||||||
|
generate_invoice)
|
||||||
|
|
||||||
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||||
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
|
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_invoice, invoice_qualified,
|
generate_invoice, invoice_qualified,
|
||||||
)
|
)
|
||||||
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW
|
||||||
|
|
||||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
|
if lock and self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TRUST_WINDOW):
|
||||||
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
||||||
# database transaction is more than enough.
|
# database transaction is more than enough.
|
||||||
lockfn = NoLockManager
|
lock = False
|
||||||
else:
|
|
||||||
lockfn = self.order.event.lock
|
|
||||||
|
|
||||||
with lockfn():
|
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
|
||||||
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
|
ignore_date=ignore_date, lock=lock)
|
||||||
ignore_date=ignore_date)
|
|
||||||
|
|
||||||
invoice = None
|
invoice = None
|
||||||
if invoice_qualified(self.order) and allow_generate_invoice:
|
if invoice_qualified(self.order) and allow_generate_invoice:
|
||||||
@@ -2611,7 +2629,7 @@ class OrderPosition(AbstractPosition):
|
|||||||
with language(self.order.locale, self.order.event.settings.region):
|
with language(self.order.locale, self.order.event.settings.region):
|
||||||
email_template = self.event.settings.mail_text_resend_link
|
email_template = self.event.settings.mail_text_resend_link
|
||||||
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
|
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
|
||||||
email_subject = self.event.settings.mail_subject_resend_link
|
email_subject = self.event.settings.mail_subject_resend_link_attendee
|
||||||
self.send_mail(
|
self.send_mail(
|
||||||
email_subject, email_template, email_context,
|
email_subject, email_template, email_context,
|
||||||
'pretix.event.order.email.resend', user=user, auth=auth,
|
'pretix.event.order.email.resend', user=user, auth=auth,
|
||||||
|
|||||||
@@ -435,28 +435,37 @@ class Voucher(LoggedModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||||
|
from ..services.locking import lock_objects
|
||||||
|
from ..services.quotas import QuotaAvailability
|
||||||
|
|
||||||
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
|
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
|
||||||
|
|
||||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||||
|
|
||||||
if quota:
|
if quota:
|
||||||
if quota in old_quotas:
|
new_quotas = {quota}
|
||||||
return
|
|
||||||
else:
|
|
||||||
avail = quota.availability(count_waitinglist=False)
|
|
||||||
elif item and item.has_variations and not variation:
|
elif item and item.has_variations and not variation:
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
'Otherwise it might be unclear which quotas to block.'))
|
||||||
elif item and variation:
|
elif item and variation:
|
||||||
avail = variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||||
elif item and not item.has_variations:
|
elif item and not item.has_variations:
|
||||||
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||||
else:
|
else:
|
||||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|
||||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
|
if not (new_quotas - old_quotas):
|
||||||
|
return
|
||||||
|
|
||||||
|
lock_objects([q for q in (new_quotas - old_quotas) if q.size is not None], shared_lock_objects=[event])
|
||||||
|
|
||||||
|
qa = QuotaAvailability(count_waitinglist=False)
|
||||||
|
qa.queue(*(new_quotas - old_quotas))
|
||||||
|
qa.compute()
|
||||||
|
|
||||||
|
if any(r[0] != Quota.AVAILABILITY_OK or (r[1] is not None and r[1] < cnt) for r in qa.results.values()):
|
||||||
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
|
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
|
||||||
'quota is currently sold out or completely reserved.'))
|
'quota is currently sold out or completely reserved.'))
|
||||||
|
|
||||||
|
|||||||
+82
-36
@@ -336,6 +336,12 @@ class BasePaymentProvider:
|
|||||||
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
||||||
required=False,
|
required=False,
|
||||||
)),
|
)),
|
||||||
|
('_availability_start',
|
||||||
|
RelativeDateField(
|
||||||
|
label=_('Available from'),
|
||||||
|
help_text=_('Users will not be able to choose this payment provider before the given date.'),
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
('_total_min',
|
('_total_min',
|
||||||
forms.DecimalField(
|
forms.DecimalField(
|
||||||
label=_('Minimum order total'),
|
label=_('Minimum order total'),
|
||||||
@@ -441,6 +447,13 @@ class BasePaymentProvider:
|
|||||||
'Share this link with customers who should use this payment method.'
|
'Share this link with customers who should use this payment method.'
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
('_prevent_reminder_mail',
|
||||||
|
forms.BooleanField(
|
||||||
|
label=_('Do not send a payment reminder mail'),
|
||||||
|
help_text=_('Users will not receive a reminder mail to pay for their order before it expires if '
|
||||||
|
'they have chosen this payment method.'),
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
d['_restricted_countries']._as_type = list
|
d['_restricted_countries']._as_type = list
|
||||||
d['_restrict_to_sales_channels']._as_type = list
|
d['_restrict_to_sales_channels']._as_type = list
|
||||||
@@ -497,6 +510,14 @@ class BasePaymentProvider:
|
|||||||
if order.status == Order.STATUS_PAID:
|
if order.status == Order.STATUS_PAID:
|
||||||
return _('paid')
|
return _('paid')
|
||||||
|
|
||||||
|
def prevent_reminder_mail(self, order: Order, payment: OrderPayment) -> bool:
|
||||||
|
"""
|
||||||
|
This is called when a periodic task runs and sends out reminder mails to orders that have not been paid yet
|
||||||
|
and are soon expiring.
|
||||||
|
The default implementation returns the content of the _prevent_reminder_mail configuration variable (a boolean value).
|
||||||
|
"""
|
||||||
|
return self.settings.get('_prevent_reminder_mail', as_type=bool, default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def payment_form_fields(self) -> dict:
|
def payment_form_fields(self) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -539,40 +560,65 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def _is_still_available(self, now_dt=None, cart_id=None, order=None):
|
def _absolute_availability_date(self, rel_date, cart_id=None, order=None, aggregate_fn=min):
|
||||||
|
if not rel_date:
|
||||||
|
return None
|
||||||
|
if self.event.has_subevents and cart_id:
|
||||||
|
dates = [
|
||||||
|
rel_date.datetime(se).date()
|
||||||
|
for se in self.event.subevents.filter(
|
||||||
|
id__in=CartPosition.objects.filter(
|
||||||
|
cart_id=cart_id, event=self.event
|
||||||
|
).values_list('subevent', flat=True)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return aggregate_fn(dates) if dates else None
|
||||||
|
elif self.event.has_subevents and order:
|
||||||
|
dates = [
|
||||||
|
rel_date.datetime(se).date()
|
||||||
|
for se in self.event.subevents.filter(
|
||||||
|
id__in=order.positions.values_list('subevent', flat=True)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return aggregate_fn(dates) if dates else None
|
||||||
|
elif self.event.has_subevents:
|
||||||
|
raise NotImplementedError('Payment provider is not subevent-ready.')
|
||||||
|
else:
|
||||||
|
return rel_date.datetime(self.event).date()
|
||||||
|
|
||||||
|
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
tz = ZoneInfo(self.event.settings.timezone)
|
tz = ZoneInfo(self.event.settings.timezone)
|
||||||
|
|
||||||
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
|
try:
|
||||||
if availability_date:
|
availability_start = self._absolute_availability_date(
|
||||||
if self.event.has_subevents and cart_id:
|
self.settings.get('_availability_start', as_type=RelativeDateWrapper),
|
||||||
dates = [
|
cart_id,
|
||||||
availability_date.datetime(se).date()
|
order,
|
||||||
for se in self.event.subevents.filter(
|
# In an event series, we use min() for the start as well. This might be inconsistent with using min() for
|
||||||
id__in=CartPosition.objects.filter(
|
# for the end, but makes it harder to put one self into a situation where no payment provider is available.
|
||||||
cart_id=cart_id, event=self.event
|
aggregate_fn=min
|
||||||
).values_list('subevent', flat=True)
|
)
|
||||||
)
|
|
||||||
]
|
|
||||||
availability_date = min(dates) if dates else None
|
|
||||||
elif self.event.has_subevents and order:
|
|
||||||
dates = [
|
|
||||||
availability_date.datetime(se).date()
|
|
||||||
for se in self.event.subevents.filter(
|
|
||||||
id__in=order.positions.values_list('subevent', flat=True)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
availability_date = min(dates) if dates else None
|
|
||||||
elif self.event.has_subevents:
|
|
||||||
logger.error('Payment provider is not subevent-ready.')
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
availability_date = availability_date.datetime(self.event).date()
|
|
||||||
|
|
||||||
if availability_date:
|
if availability_start:
|
||||||
return availability_date >= now_dt.astimezone(tz).date()
|
if availability_start > now_dt.astimezone(tz).date():
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
availability_end = self._absolute_availability_date(
|
||||||
|
self.settings.get('_availability_date', as_type=RelativeDateWrapper),
|
||||||
|
cart_id,
|
||||||
|
order,
|
||||||
|
aggregate_fn=min
|
||||||
|
)
|
||||||
|
|
||||||
|
if availability_end:
|
||||||
|
if availability_end < now_dt.astimezone(tz).date():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
except NotImplementedError:
|
||||||
|
logger.exception('Unable to check availability')
|
||||||
|
return False
|
||||||
|
|
||||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -581,9 +627,9 @@ class BasePaymentProvider:
|
|||||||
user will not be able to select this payment method. This will only be called
|
user will not be able to select this payment method. This will only be called
|
||||||
during checkout, not on retrying.
|
during checkout, not on retrying.
|
||||||
|
|
||||||
The default implementation checks for the _availability_date setting to be either unset or in the future
|
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future
|
||||||
and for the _total_max and _total_min requirements to be met. It also checks the ``_restrict_countries``
|
and for the ``_availability_from``, ``_total_max``, and ``_total_min`` requirements to be met. It also checks
|
||||||
and ``_restrict_to_sales_channels`` setting.
|
the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
|
||||||
|
|
||||||
:param total: The total value without the payment method fee, after taxes.
|
:param total: The total value without the payment method fee, after taxes.
|
||||||
|
|
||||||
@@ -592,7 +638,7 @@ class BasePaymentProvider:
|
|||||||
The ``total`` parameter has been added. For backwards compatibility, this method is called again
|
The ``total`` parameter has been added. For backwards compatibility, this method is called again
|
||||||
without this parameter if it raises a ``TypeError`` on first try.
|
without this parameter if it raises a ``TypeError`` on first try.
|
||||||
"""
|
"""
|
||||||
timing = self._is_still_available(cart_id=get_or_create_cart_id(request))
|
timing = self._is_available_by_time(cart_id=get_or_create_cart_id(request))
|
||||||
pricing = True
|
pricing = True
|
||||||
|
|
||||||
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
|
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
|
||||||
@@ -776,8 +822,8 @@ class BasePaymentProvider:
|
|||||||
Will be called to check whether it is allowed to change the payment method of
|
Will be called to check whether it is allowed to change the payment method of
|
||||||
an order to this one.
|
an order to this one.
|
||||||
|
|
||||||
The default implementation checks for the _availability_date setting to be either unset or in the future,
|
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future,
|
||||||
as well as for the _total_max, _total_min and _restricted_countries settings.
|
as well as for the ``_availability_from``, ``_total_max``, ``_total_min``, and ``_restricted_countries`` settings.
|
||||||
|
|
||||||
:param order: The order object
|
:param order: The order object
|
||||||
"""
|
"""
|
||||||
@@ -804,7 +850,7 @@ class BasePaymentProvider:
|
|||||||
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._is_still_available(order=order)
|
return self._is_available_by_time(order=order)
|
||||||
|
|
||||||
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
|
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ def images_from_questions(sender, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
a = op.answers.filter(question_id=question_id).first() or a
|
a = op.answers.filter(question_id=question_id).first() or a
|
||||||
|
|
||||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
|
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if etag:
|
if etag:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import uuid
|
|||||||
from collections import Counter, defaultdict, namedtuple
|
from collections import Counter, defaultdict, namedtuple
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from time import sleep
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
@@ -62,7 +63,7 @@ from pretix.base.models.orders import OrderFee
|
|||||||
from pretix.base.models.tax import TaxRule
|
from pretix.base.models.tax import TaxRule
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.services.checkin import _save_answers
|
from pretix.base.services.checkin import _save_answers
|
||||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
from pretix.base.services.locking import LockTimeoutException, lock_objects
|
||||||
from pretix.base.services.pricing import (
|
from pretix.base.services.pricing import (
|
||||||
apply_discounts, get_line_price, get_listed_price, get_price,
|
apply_discounts, get_line_price, get_listed_price, get_price,
|
||||||
is_included_for_free,
|
is_included_for_free,
|
||||||
@@ -76,6 +77,7 @@ from pretix.celery_app import app
|
|||||||
from pretix.presale.signals import (
|
from pretix.presale.signals import (
|
||||||
checkout_confirm_messages, fee_calculation_for_cart,
|
checkout_confirm_messages, fee_calculation_for_cart,
|
||||||
)
|
)
|
||||||
|
from pretix.testutils.middleware import debugflags_var
|
||||||
|
|
||||||
|
|
||||||
class CartError(Exception):
|
class CartError(Exception):
|
||||||
@@ -1073,7 +1075,20 @@ class CartManager:
|
|||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
@transaction.atomic(durable=True)
|
||||||
def _perform_operations(self):
|
def _perform_operations(self):
|
||||||
|
full_lock_required = any(getattr(o, 'seat', False) for o in self._operations) and self.event.settings.seating_minimal_distance > 0
|
||||||
|
if full_lock_required:
|
||||||
|
# We lock the entire event in this case since we don't want to deal with fine-granular locking
|
||||||
|
# in the case of seating distance enforcement
|
||||||
|
lock_objects([self.event])
|
||||||
|
else:
|
||||||
|
lock_objects(
|
||||||
|
[q for q, d in self._quota_diff.items() if q.size is not None and d > 0] +
|
||||||
|
[v for v, d in self._voucher_use_diff.items() if d > 0] +
|
||||||
|
[getattr(o, 'seat', False) for o in self._operations if getattr(o, 'seat', False)],
|
||||||
|
shared_lock_objects=[self.event]
|
||||||
|
)
|
||||||
vouchers_ok = self._get_voucher_availability()
|
vouchers_ok = self._get_voucher_availability()
|
||||||
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
|
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
|
||||||
err = None
|
err = None
|
||||||
@@ -1085,6 +1100,9 @@ class CartManager:
|
|||||||
self._operations.sort(key=lambda a: self.order[type(a)])
|
self._operations.sort(key=lambda a: self.order[type(a)])
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
|
|
||||||
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
for iop, op in enumerate(self._operations):
|
for iop, op in enumerate(self._operations):
|
||||||
if isinstance(op, self.RemoveOperation):
|
if isinstance(op, self.RemoveOperation):
|
||||||
if op.position.expires > self.now_dt:
|
if op.position.expires > self.now_dt:
|
||||||
@@ -1289,22 +1307,11 @@ class CartManager:
|
|||||||
p.save()
|
p.save()
|
||||||
_save_answers(p, {}, p._answers)
|
_save_answers(p, {}, p._answers)
|
||||||
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
|
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
|
||||||
|
|
||||||
|
if 'sleep-before-commit' in debugflags_var.get():
|
||||||
|
sleep(2)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _require_locking(self):
|
|
||||||
if self._voucher_use_diff:
|
|
||||||
# If any vouchers are used, we lock to make sure we don't redeem them to often
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self._quota_diff and any(q.size is not None for q in self._quota_diff):
|
|
||||||
# If any quotas are affected that are not unlimited, we lock
|
|
||||||
return True
|
|
||||||
|
|
||||||
if any(getattr(o, 'seat', False) for o in self._operations):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def recompute_final_prices_and_taxes(self):
|
def recompute_final_prices_and_taxes(self):
|
||||||
positions = sorted(list(self.positions), key=lambda op: -(op.addon_to_id or 0))
|
positions = sorted(list(self.positions), key=lambda op: -(op.addon_to_id or 0))
|
||||||
diff = Decimal('0.00')
|
diff = Decimal('0.00')
|
||||||
@@ -1343,18 +1350,14 @@ class CartManager:
|
|||||||
err = self.extend_expired_positions() or err
|
err = self.extend_expired_positions() or err
|
||||||
err = err or self._check_min_per_voucher()
|
err = err or self._check_min_per_voucher()
|
||||||
|
|
||||||
lockfn = NoLockManager
|
self.now_dt = now()
|
||||||
if self._require_locking():
|
|
||||||
lockfn = self.event.lock
|
|
||||||
|
|
||||||
with lockfn() as now_dt:
|
self._extend_expiry_of_valid_existing_positions()
|
||||||
with transaction.atomic():
|
err = self._perform_operations() or err
|
||||||
self.now_dt = now_dt
|
self.recompute_final_prices_and_taxes()
|
||||||
self._extend_expiry_of_valid_existing_positions()
|
|
||||||
err = self._perform_operations() or err
|
if err:
|
||||||
self.recompute_final_prices_and_taxes()
|
raise CartError(err)
|
||||||
if err:
|
|
||||||
raise CartError(err)
|
|
||||||
|
|
||||||
|
|
||||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ def _build_time(t=None, value=None, ev=None, now_dt=None):
|
|||||||
|
|
||||||
|
|
||||||
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
||||||
logic_environment = _get_logic_environment(ev, now_dt)
|
logic_environment = _get_logic_environment(ev, rule_data, now_dt)
|
||||||
event = ev if isinstance(ev, Event) else ev.event
|
event = ev if isinstance(ev, Event) else ev.event
|
||||||
|
|
||||||
def _evaluate_inners(r):
|
def _evaluate_inners(r):
|
||||||
@@ -112,6 +112,8 @@ def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
|||||||
val = str(event.items.get(pk=val))
|
val = str(event.items.get(pk=val))
|
||||||
elif var == "variation":
|
elif var == "variation":
|
||||||
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
||||||
|
elif var == "gate":
|
||||||
|
val = str(event.organizer.gates.get(pk=val))
|
||||||
elif isinstance(val, datetime):
|
elif isinstance(val, datetime):
|
||||||
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
||||||
return {"var": var, "__result": val}
|
return {"var": var, "__result": val}
|
||||||
@@ -152,7 +154,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
||||||
"""
|
"""
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
logic_environment = _get_logic_environment(ev, now_dt)
|
logic_environment = _get_logic_environment(ev, rule_data, now_dt)
|
||||||
_var_values = {'False': False, 'True': True}
|
_var_values = {'False': False, 'True': True}
|
||||||
_var_explanations = {}
|
_var_explanations = {}
|
||||||
|
|
||||||
@@ -174,15 +176,22 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
_var_values[new_var_name] = result
|
_var_values[new_var_name] = result
|
||||||
if not result:
|
if not result:
|
||||||
# Operator returned false, let's dig deeper
|
# Operator returned false, let's dig deeper
|
||||||
if "var" not in values[0]:
|
if "var" in values[0]:
|
||||||
|
if isinstance(values[0]["var"], list):
|
||||||
|
values[0]["var"] = values[0]["var"][0]
|
||||||
|
_var_explanations[new_var_name] = {
|
||||||
|
'operator': operator,
|
||||||
|
'var': values[0]["var"],
|
||||||
|
'rhs': values[1:],
|
||||||
|
}
|
||||||
|
elif "entries_since" in values[0] or "entries_before" in values[0]:
|
||||||
|
_var_explanations[new_var_name] = {
|
||||||
|
'operator': operator,
|
||||||
|
'var': values[0],
|
||||||
|
'rhs': values[1:],
|
||||||
|
}
|
||||||
|
else:
|
||||||
raise ValueError("Binary operators should be normalized to have a variable on their left-hand side")
|
raise ValueError("Binary operators should be normalized to have a variable on their left-hand side")
|
||||||
if isinstance(values[0]["var"], list):
|
|
||||||
values[0]["var"] = values[0]["var"][0]
|
|
||||||
_var_explanations[new_var_name] = {
|
|
||||||
'operator': operator,
|
|
||||||
'var': values[0]["var"],
|
|
||||||
'rhs': values[1:],
|
|
||||||
}
|
|
||||||
return {'var': new_var_name}
|
return {'var': new_var_name}
|
||||||
try:
|
try:
|
||||||
rules = _evaluate_inners(rules)
|
rules = _evaluate_inners(rules)
|
||||||
@@ -249,11 +258,17 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
elif var == 'product' or var == 'variation':
|
elif var == 'product' or var == 'variation':
|
||||||
var_weights[vname] = (1000, 0)
|
var_weights[vname] = (1000, 0)
|
||||||
var_texts[vname] = _('Ticket type not allowed')
|
var_texts[vname] = _('Ticket type not allowed')
|
||||||
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday'):
|
elif var == 'gate':
|
||||||
|
var_weights[vname] = (500, 0)
|
||||||
|
var_texts[vname] = _('Wrong entrance gate')
|
||||||
|
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
|
||||||
|
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
|
||||||
w = {
|
w = {
|
||||||
'minutes_since_first_entry': 80,
|
'minutes_since_first_entry': 80,
|
||||||
'minutes_since_last_entry': 90,
|
'minutes_since_last_entry': 90,
|
||||||
'entries_days': 100,
|
'entries_days': 100,
|
||||||
|
'entries_since': 110,
|
||||||
|
'entries_before': 110,
|
||||||
'entries_number': 120,
|
'entries_number': 120,
|
||||||
'entries_today': 140,
|
'entries_today': 140,
|
||||||
'now_isoweekday': 210,
|
'now_isoweekday': 210,
|
||||||
@@ -272,8 +287,24 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
'entries_days': _('number of days with an entry'),
|
'entries_days': _('number of days with an entry'),
|
||||||
'entries_number': _('number of entries'),
|
'entries_number': _('number of entries'),
|
||||||
'entries_today': _('number of entries today'),
|
'entries_today': _('number of entries today'),
|
||||||
|
'entries_since': _('number of entries since {datetime}'),
|
||||||
|
'entries_before': _('number of entries before {datetime}'),
|
||||||
'now_isoweekday': _('week day'),
|
'now_isoweekday': _('week day'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
|
||||||
|
varname = list(var.keys())[0]
|
||||||
|
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
||||||
|
if abs(now_dt - cutoff) < timedelta(hours=12):
|
||||||
|
compare_to_text = date_format(cutoff, 'TIME_FORMAT')
|
||||||
|
else:
|
||||||
|
compare_to_text = date_format(cutoff, 'SHORT_DATETIME_FORMAT')
|
||||||
|
l[varname] = str(l[varname].format(datetime=compare_to_text))
|
||||||
|
var = varname
|
||||||
|
var_result = getattr(rule_data, var)(cutoff)
|
||||||
|
else:
|
||||||
|
var_result = rule_data[var]
|
||||||
|
|
||||||
compare_to = rhs[0]
|
compare_to = rhs[0]
|
||||||
penalty = 0
|
penalty = 0
|
||||||
|
|
||||||
@@ -290,7 +321,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
# These are "technical" comparisons without real meaning, we don't want to show them.
|
# These are "technical" comparisons without real meaning, we don't want to show them.
|
||||||
penalty = 1000
|
penalty = 1000
|
||||||
|
|
||||||
var_weights[vname] = (w[var] + operator_weights.get(operator, 0) + penalty, abs(compare_to - rule_data[var]))
|
var_weights[vname] = (w[var] + operator_weights.get(operator, 0) + penalty, abs(compare_to - var_result))
|
||||||
|
|
||||||
if var == 'now_isoweekday':
|
if var == 'now_isoweekday':
|
||||||
compare_to = {
|
compare_to = {
|
||||||
@@ -337,12 +368,14 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
|
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
|
||||||
|
|
||||||
|
|
||||||
def _get_logic_environment(ev, now_dt):
|
def _get_logic_environment(ev, rule_data, now_dt):
|
||||||
# Every change to our supported JSON logic must be done
|
# Every change to our supported JSON logic must be done
|
||||||
# * in pretix.base.services.checkin
|
# * in pretix.base.services.checkin
|
||||||
# * in pretix.base.models.checkin
|
# * in pretix.base.models.checkin
|
||||||
|
# * in pretix.helpers.jsonlogic_boolalg
|
||||||
# * in checkinrules.js
|
# * in checkinrules.js
|
||||||
# * in libpretixsync
|
# * in libpretixsync
|
||||||
|
# * in pretixscan-ios
|
||||||
|
|
||||||
def is_before(t1, t2, tolerance=None):
|
def is_before(t1, t2, tolerance=None):
|
||||||
if tolerance:
|
if tolerance:
|
||||||
@@ -357,14 +390,18 @@ def _get_logic_environment(ev, now_dt):
|
|||||||
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
|
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
|
||||||
logic.add_operation('isBefore', is_before)
|
logic.add_operation('isBefore', is_before)
|
||||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
||||||
|
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
|
||||||
|
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
|
||||||
return logic
|
return logic
|
||||||
|
|
||||||
|
|
||||||
class LazyRuleVars:
|
class LazyRuleVars:
|
||||||
def __init__(self, position, clist, dt):
|
def __init__(self, position, clist, dt, gate):
|
||||||
self._position = position
|
self._position = position
|
||||||
self._clist = clist
|
self._clist = clist
|
||||||
self._dt = dt
|
self._dt = dt
|
||||||
|
self._gate = gate
|
||||||
|
self.__cache = {}
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
if item[0] != '_' and hasattr(self, item):
|
if item[0] != '_' and hasattr(self, item):
|
||||||
@@ -380,6 +417,10 @@ class LazyRuleVars:
|
|||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
return self._dt.astimezone(tz).isoweekday()
|
return self._dt.astimezone(tz).isoweekday()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gate(self):
|
||||||
|
return self._gate.pk if self._gate else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def product(self):
|
def product(self):
|
||||||
return self._position.item_id
|
return self._position.item_id
|
||||||
@@ -398,6 +439,16 @@ class LazyRuleVars:
|
|||||||
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
|
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
|
||||||
|
|
||||||
|
def entries_since(self, cutoff):
|
||||||
|
if ('entries_since', cutoff) not in self.__cache:
|
||||||
|
self.__cache['entries_since', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=cutoff).count()
|
||||||
|
return self.__cache['entries_since', cutoff]
|
||||||
|
|
||||||
|
def entries_before(self, cutoff):
|
||||||
|
if ('entries_before', cutoff) not in self.__cache:
|
||||||
|
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
|
||||||
|
return self.__cache['entries_before', cutoff]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def entries_days(self):
|
def entries_days(self):
|
||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
@@ -446,8 +497,9 @@ class SQLLogic:
|
|||||||
* Comparison operators (==, !=, …) never contain boolean operators (and, or) further down in the stack
|
* Comparison operators (==, !=, …) never contain boolean operators (and, or) further down in the stack
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, list):
|
def __init__(self, list, gate=None):
|
||||||
self.list = list
|
self.list = list
|
||||||
|
self.gate = gate
|
||||||
self.bool_ops = {
|
self.bool_ops = {
|
||||||
"and": lambda *args: reduce(lambda total, arg: total & arg, args) if args else Q(),
|
"and": lambda *args: reduce(lambda total, arg: total & arg, args) if args else Q(),
|
||||||
"or": lambda *args: reduce(lambda total, arg: total | arg, args) if args else Q(),
|
"or": lambda *args: reduce(lambda total, arg: total | arg, args) if args else Q(),
|
||||||
@@ -463,7 +515,7 @@ class SQLLogic:
|
|||||||
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
||||||
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
||||||
}
|
}
|
||||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var'}
|
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
|
||||||
|
|
||||||
def operation_to_expression(self, rule):
|
def operation_to_expression(self, rule):
|
||||||
if not isinstance(rule, dict):
|
if not isinstance(rule, dict):
|
||||||
@@ -511,6 +563,36 @@ class SQLLogic:
|
|||||||
return [self.operation_to_expression(v) for v in values]
|
return [self.operation_to_expression(v) for v in values]
|
||||||
elif operator == 'lookup':
|
elif operator == 'lookup':
|
||||||
return int(values[1])
|
return int(values[1])
|
||||||
|
elif operator == 'entries_since':
|
||||||
|
return Coalesce(
|
||||||
|
Subquery(
|
||||||
|
Checkin.objects.filter(
|
||||||
|
position_id=OuterRef('pk'),
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list_id=self.list.pk,
|
||||||
|
datetime__gte=self.operation_to_expression(values[0]),
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('*')
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
|
elif operator == 'entries_before':
|
||||||
|
return Coalesce(
|
||||||
|
Subquery(
|
||||||
|
Checkin.objects.filter(
|
||||||
|
position_id=OuterRef('pk'),
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list_id=self.list.pk,
|
||||||
|
datetime__lt=self.operation_to_expression(values[0]),
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('*')
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
elif operator == 'var':
|
elif operator == 'var':
|
||||||
if values[0] == 'now':
|
if values[0] == 'now':
|
||||||
return Value(now().astimezone(timezone.utc))
|
return Value(now().astimezone(timezone.utc))
|
||||||
@@ -520,6 +602,8 @@ class SQLLogic:
|
|||||||
return F('item_id')
|
return F('item_id')
|
||||||
elif values[0] == 'variation':
|
elif values[0] == 'variation':
|
||||||
return F('variation_id')
|
return F('variation_id')
|
||||||
|
elif values[0] == 'gate':
|
||||||
|
return Value(self.gate.pk if self.gate else None)
|
||||||
elif values[0] == 'entries_number':
|
elif values[0] == 'entries_number':
|
||||||
return Coalesce(
|
return Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
@@ -731,7 +815,8 @@ def _save_answers(op, answers, given_answers):
|
|||||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False):
|
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
||||||
|
gate=None):
|
||||||
"""
|
"""
|
||||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||||
not valid at this time.
|
not valid at this time.
|
||||||
@@ -746,6 +831,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
:param nonce: A random nonce to prevent race conditions.
|
:param nonce: A random nonce to prevent race conditions.
|
||||||
:param datetime: The datetime of the checkin, defaults to now.
|
:param datetime: The datetime of the checkin, defaults to now.
|
||||||
:param simulate: If true, the check-in is not saved.
|
:param simulate: If true, the check-in is not saved.
|
||||||
|
:param gate: The gate the check-in was performed at.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# !!!!!!!!!
|
# !!!!!!!!!
|
||||||
@@ -860,8 +946,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
if type == Checkin.TYPE_ENTRY and clist.rules:
|
if type == Checkin.TYPE_ENTRY and clist.rules:
|
||||||
rule_data = LazyRuleVars(op, clist, dt)
|
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
|
||||||
logic = _get_logic_environment(op.subevent or clist.event, now_dt=dt)
|
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
|
||||||
if not logic.apply(clist.rules, rule_data):
|
if not logic.apply(clist.rules, rule_data):
|
||||||
if force:
|
if force:
|
||||||
force_used = True
|
force_used = True
|
||||||
@@ -885,6 +971,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
device = None
|
device = None
|
||||||
if isinstance(auth, Device):
|
if isinstance(auth, Device):
|
||||||
device = auth
|
device = auth
|
||||||
|
if not gate:
|
||||||
|
gate = device.gate
|
||||||
|
|
||||||
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce', 'position_id'))
|
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce', 'position_id'))
|
||||||
entry_allowed = (
|
entry_allowed = (
|
||||||
@@ -908,7 +996,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
list=clist,
|
list=clist,
|
||||||
datetime=dt,
|
datetime=dt,
|
||||||
device=device,
|
device=device,
|
||||||
gate=device.gate if device else None,
|
gate=gate,
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||||
force_sent=force,
|
force_sent=force,
|
||||||
|
|||||||
@@ -20,31 +20,105 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
||||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
||||||
#
|
|
||||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
||||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
||||||
#
|
|
||||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
from itertools import groupby
|
||||||
import uuid
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import DatabaseError, connection
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from pretix.base.models import EventLock
|
from pretix.base.models import Event, Membership, Quota, Seat, Voucher
|
||||||
|
from pretix.testutils.middleware import debugflags_var
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.locking')
|
logger = logging.getLogger('pretix.base.locking')
|
||||||
LOCK_TIMEOUT = 120
|
|
||||||
|
# A lock acquisition is aborted if it takes longer than LOCK_ACQUISITION_TIMEOUT to prevent connection starvation
|
||||||
|
LOCK_ACQUISITION_TIMEOUT = 3
|
||||||
|
|
||||||
|
# We make the assumption that it is safe to e.g. transform an order into a cart if the order has a lifetime of more than
|
||||||
|
# LOCK_TRUST_WINDOW into the future. In other words, we assume that a lock is never held longer than LOCK_TRUST_WINDOW.
|
||||||
|
# This assumption holds true for all in-request locks, since our gunicorn default settings kill a worker that takes
|
||||||
|
# longer than 60 seconds to process a request. It however does not hold true for celery tasks, especially long-running
|
||||||
|
# ones, so this does introduce *some* risk of incorrect locking.
|
||||||
|
LOCK_TRUST_WINDOW = 120
|
||||||
|
|
||||||
|
# These are different offsets for the different types of keys we want to lock
|
||||||
|
KEY_SPACES = {
|
||||||
|
Event: 1,
|
||||||
|
Quota: 2,
|
||||||
|
Seat: 3,
|
||||||
|
Voucher: 4,
|
||||||
|
Membership: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pg_lock_key(obj):
|
||||||
|
"""
|
||||||
|
This maps the primary key space of multiple tables to a single bigint key space within postgres. It is not
|
||||||
|
an injective function, which is fine, as long as collisions are rare.
|
||||||
|
"""
|
||||||
|
keyspace = KEY_SPACES.get(type(obj))
|
||||||
|
objectid = obj.pk
|
||||||
|
if not keyspace:
|
||||||
|
raise ValueError(f"No key space defined for locking objects of type {type(obj)}")
|
||||||
|
assert isinstance(objectid, int)
|
||||||
|
# 64bit int: xxxxxxxx xxxxxxx xxxxxxx xxxxxxx xxxxxx xxxxxxx xxxxxxx xxxxxxx
|
||||||
|
# | objectid mod 2**48 | |index| |keysp.|
|
||||||
|
key = ((objectid % 281474976710656) << 16) | ((settings.DATABASE_ADVISORY_LOCK_INDEX % 256) << 8) | (keyspace % 256)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
class LockTimeoutException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def lock_objects(objects, *, shared_lock_objects=None, replace_exclusive_with_shared_when_exclusive_are_more_than=20):
|
||||||
|
"""
|
||||||
|
Create an exclusive lock on the objects passed in `objects`. This function MUST be called within an atomic
|
||||||
|
transaction and SHOULD be called only once per transaction to prevent deadlocks.
|
||||||
|
|
||||||
|
A shared lock will be created on objects passed in `shared_lock_objects`.
|
||||||
|
|
||||||
|
If `objects` contains more than `replace_exclusive_with_shared_when_exclusive_are_more_than` objects, `objects`
|
||||||
|
will be ignored and `shared_lock_objects` will be used in its place and receive an exclusive lock.
|
||||||
|
|
||||||
|
The idea behind it is this: Usually we create a lock on every quota, voucher, or seat contained in an order.
|
||||||
|
However, this has a large performance penalty in case we have hundreds of locks required. Therefore, we always
|
||||||
|
place a shared lock in the event, and if we have too many affected objects, we fall back to event-level locks.
|
||||||
|
"""
|
||||||
|
if (not objects and not shared_lock_objects) or 'skip-locking' in debugflags_var.get():
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'fail-locking' in debugflags_var.get():
|
||||||
|
raise LockTimeoutException()
|
||||||
|
|
||||||
|
if not connection.in_atomic_block:
|
||||||
|
raise RuntimeError(
|
||||||
|
"You cannot create locks outside of an transaction"
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
|
||||||
|
shared_keys = set(pg_lock_key(obj) for obj in shared_lock_objects) if shared_lock_objects else set()
|
||||||
|
exclusive_keys = set(pg_lock_key(obj) for obj in objects)
|
||||||
|
if replace_exclusive_with_shared_when_exclusive_are_more_than and len(exclusive_keys) > replace_exclusive_with_shared_when_exclusive_are_more_than:
|
||||||
|
exclusive_keys = shared_keys
|
||||||
|
keys = sorted(list(shared_keys | exclusive_keys))
|
||||||
|
calls = ", ".join([
|
||||||
|
(f"pg_advisory_xact_lock({k})" if k in exclusive_keys else f"pg_advisory_xact_lock_shared({k})") for k in keys
|
||||||
|
])
|
||||||
|
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f"SET LOCAL lock_timeout = '{LOCK_ACQUISITION_TIMEOUT}s';")
|
||||||
|
cursor.execute(f"SELECT {calls};")
|
||||||
|
cursor.execute("SET LOCAL lock_timeout = '0';") # back to default
|
||||||
|
except DatabaseError as e:
|
||||||
|
logger.warning(f"Waiting for locks timed out: {e} on SELECT {calls};")
|
||||||
|
raise LockTimeoutException()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for model, instances in groupby(objects, key=lambda o: type(o)):
|
||||||
|
model.objects.select_for_update().filter(pk__in=[o.pk for o in instances])
|
||||||
|
|
||||||
|
|
||||||
class NoLockManager:
|
class NoLockManager:
|
||||||
@@ -57,128 +131,3 @@ class NoLockManager:
|
|||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
if exc_type is not None:
|
if exc_type is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class LockManager:
|
|
||||||
def __init__(self, event):
|
|
||||||
self.event = event
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
lock_event(self.event)
|
|
||||||
return now()
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
release_event(self.event)
|
|
||||||
if exc_type is not None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class LockTimeoutException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LockReleaseException(LockTimeoutException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def lock_event(event):
|
|
||||||
"""
|
|
||||||
Issue a lock on this event so nobody can book tickets for this event until
|
|
||||||
you release the lock. Will retry 5 times on failure.
|
|
||||||
|
|
||||||
:raises LockTimeoutException: if the event is locked every time we try
|
|
||||||
to obtain the lock
|
|
||||||
"""
|
|
||||||
if hasattr(event, '_lock') and event._lock:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if settings.HAS_REDIS:
|
|
||||||
return lock_event_redis(event)
|
|
||||||
else:
|
|
||||||
return lock_event_db(event)
|
|
||||||
|
|
||||||
|
|
||||||
def release_event(event):
|
|
||||||
"""
|
|
||||||
Release a lock placed by :py:meth:`lock()`. If the parameter force is not set to ``True``,
|
|
||||||
the lock will only be released if it was issued in _this_ python
|
|
||||||
representation of the database object.
|
|
||||||
|
|
||||||
:raises LockReleaseException: if we do not own the lock
|
|
||||||
"""
|
|
||||||
if not hasattr(event, '_lock') or not event._lock:
|
|
||||||
raise LockReleaseException('Lock is not owned by this thread')
|
|
||||||
if settings.HAS_REDIS:
|
|
||||||
return release_event_redis(event)
|
|
||||||
else:
|
|
||||||
return release_event_db(event)
|
|
||||||
|
|
||||||
|
|
||||||
def lock_event_db(event):
|
|
||||||
retries = 5
|
|
||||||
for i in range(retries):
|
|
||||||
with transaction.atomic():
|
|
||||||
dt = now()
|
|
||||||
l, created = EventLock.objects.get_or_create(event=event.id)
|
|
||||||
if created:
|
|
||||||
event._lock = l
|
|
||||||
return True
|
|
||||||
elif l.date < now() - timedelta(seconds=LOCK_TIMEOUT):
|
|
||||||
newtoken = str(uuid.uuid4())
|
|
||||||
updated = EventLock.objects.filter(event=event.id, token=l.token).update(date=dt, token=newtoken)
|
|
||||||
if updated:
|
|
||||||
l.token = newtoken
|
|
||||||
event._lock = l
|
|
||||||
return True
|
|
||||||
time.sleep(2 ** i / 100)
|
|
||||||
raise LockTimeoutException()
|
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def release_event_db(event):
|
|
||||||
if not hasattr(event, '_lock') or not event._lock:
|
|
||||||
raise LockReleaseException('Lock is not owned by this thread')
|
|
||||||
try:
|
|
||||||
lock = EventLock.objects.get(event=event.id, token=event._lock.token)
|
|
||||||
lock.delete()
|
|
||||||
event._lock = None
|
|
||||||
except EventLock.DoesNotExist:
|
|
||||||
raise LockReleaseException('Lock is no longer owned by this thread')
|
|
||||||
|
|
||||||
|
|
||||||
def redis_lock_from_event(event):
|
|
||||||
from django_redis import get_redis_connection
|
|
||||||
from redis.lock import Lock
|
|
||||||
|
|
||||||
if not hasattr(event, '_lock') or not event._lock:
|
|
||||||
rc = get_redis_connection("redis")
|
|
||||||
event._lock = Lock(redis=rc, name='pretix_event_%s' % event.id, timeout=LOCK_TIMEOUT)
|
|
||||||
return event._lock
|
|
||||||
|
|
||||||
|
|
||||||
def lock_event_redis(event):
|
|
||||||
from redis.exceptions import RedisError
|
|
||||||
|
|
||||||
lock = redis_lock_from_event(event)
|
|
||||||
retries = 5
|
|
||||||
for i in range(retries):
|
|
||||||
try:
|
|
||||||
if lock.acquire(blocking=False):
|
|
||||||
return True
|
|
||||||
except RedisError:
|
|
||||||
logger.exception('Error locking an event')
|
|
||||||
raise LockTimeoutException()
|
|
||||||
time.sleep(2 ** i / 100)
|
|
||||||
raise LockTimeoutException()
|
|
||||||
|
|
||||||
|
|
||||||
def release_event_redis(event):
|
|
||||||
from redis import RedisError
|
|
||||||
|
|
||||||
lock = redis_lock_from_event(event)
|
|
||||||
try:
|
|
||||||
lock.release()
|
|
||||||
except RedisError:
|
|
||||||
logger.exception('Error releasing an event lock')
|
|
||||||
raise LockReleaseException()
|
|
||||||
event._lock = None
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.orders import Transaction
|
from pretix.base.models.orders import Transaction
|
||||||
from pretix.base.orderimport import get_all_columns
|
from pretix.base.orderimport import get_all_columns
|
||||||
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
||||||
from pretix.base.services.locking import NoLockManager
|
from pretix.base.services.locking import lock_objects
|
||||||
from pretix.base.services.tasks import ProfiledEventTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.base.signals import order_paid, order_placed
|
from pretix.base.signals import order_paid, order_placed
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
@@ -88,7 +88,6 @@ def setif(record, obj, attr, setting):
|
|||||||
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
||||||
cf = CachedFile.objects.get(id=fileid)
|
cf = CachedFile.objects.get(id=fileid)
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
seats_used = False
|
|
||||||
with language(locale, event.settings.region):
|
with language(locale, event.settings.region):
|
||||||
cols = get_all_columns(event)
|
cols = get_all_columns(event)
|
||||||
parsed = parse_csv(cf.file)
|
parsed = parse_csv(cf.file)
|
||||||
@@ -118,6 +117,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
|
|
||||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||||
# shorter. We'll see what works better in reality…
|
# shorter. We'll see what works better in reality…
|
||||||
|
lock_seats = []
|
||||||
for i, record in enumerate(data):
|
for i, record in enumerate(data):
|
||||||
try:
|
try:
|
||||||
if order is None or settings['orders'] == 'many':
|
if order is None or settings['orders'] == 'many':
|
||||||
@@ -135,7 +135,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||||
position.meta_info = {}
|
position.meta_info = {}
|
||||||
if position.seat is not None:
|
if position.seat is not None:
|
||||||
seats_used = True
|
lock_seats.append(position.seat)
|
||||||
order._positions.append(position)
|
order._positions.append(position)
|
||||||
position.assign_pseudonymization_id()
|
position.assign_pseudonymization_id()
|
||||||
|
|
||||||
@@ -147,12 +147,15 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats
|
|
||||||
# are in use
|
|
||||||
lockfn = event.lock if seats_used else NoLockManager
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with lockfn(), transaction.atomic():
|
with transaction.atomic():
|
||||||
|
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats are in use
|
||||||
|
if lock_seats:
|
||||||
|
lock_objects(lock_seats, shared_lock_objects=[event])
|
||||||
|
for s in lock_seats:
|
||||||
|
if not s.is_available():
|
||||||
|
raise ImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
||||||
|
|
||||||
save_transactions = []
|
save_transactions = []
|
||||||
for o in orders:
|
for o in orders:
|
||||||
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
||||||
|
|||||||
+233
-186
@@ -35,10 +35,13 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
import sys
|
import sys
|
||||||
from collections import Counter, defaultdict, namedtuple
|
from collections import Counter, defaultdict, namedtuple
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from functools import reduce
|
||||||
|
from time import sleep
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
@@ -82,7 +85,9 @@ from pretix.base.services import tickets
|
|||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_qualified,
|
||||||
)
|
)
|
||||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
from pretix.base.services.locking import (
|
||||||
|
LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects,
|
||||||
|
)
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.memberships import (
|
from pretix.base.services.memberships import (
|
||||||
create_membership, validate_memberships_in_order,
|
create_membership, validate_memberships_in_order,
|
||||||
@@ -102,6 +107,7 @@ from pretix.celery_app import app
|
|||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
|
from pretix.testutils.middleware import debugflags_var
|
||||||
|
|
||||||
|
|
||||||
class OrderError(Exception):
|
class OrderError(Exception):
|
||||||
@@ -209,9 +215,9 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
if order.status != Order.STATUS_CANCELED:
|
if order.status != Order.STATUS_CANCELED:
|
||||||
raise OrderError(_('The order was not canceled.'))
|
raise OrderError(_('The order was not canceled.'))
|
||||||
|
|
||||||
with order.event.lock() as now_dt:
|
with transaction.atomic():
|
||||||
is_available = order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True,
|
is_available = order._is_still_available(now(), count_waitinglist=False, check_voucher_usage=True,
|
||||||
check_memberships=True, force=force)
|
check_memberships=True, lock=True, force=force)
|
||||||
if is_available is True:
|
if is_available is True:
|
||||||
if order.payment_refund_sum >= order.total:
|
if order.payment_refund_sum >= order.total:
|
||||||
order.status = Order.STATUS_PAID
|
order.status = Order.STATUS_PAID
|
||||||
@@ -220,29 +226,28 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
order.cancellation_date = None
|
order.cancellation_date = None
|
||||||
order.set_expires(now(),
|
order.set_expires(now(),
|
||||||
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||||
with transaction.atomic():
|
order.save(update_fields=['expires', 'status', 'cancellation_date'])
|
||||||
order.save(update_fields=['expires', 'status', 'cancellation_date'])
|
order.log_action(
|
||||||
order.log_action(
|
'pretix.event.order.reactivated',
|
||||||
'pretix.event.order.reactivated',
|
user=user,
|
||||||
user=user,
|
auth=auth,
|
||||||
auth=auth,
|
data={
|
||||||
data={
|
'expires': order.expires,
|
||||||
'expires': order.expires,
|
}
|
||||||
}
|
)
|
||||||
)
|
for position in order.positions.all():
|
||||||
for position in order.positions.all():
|
if position.voucher:
|
||||||
if position.voucher:
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
|
||||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
|
|
||||||
|
|
||||||
for gc in position.issued_gift_cards.all():
|
for gc in position.issued_gift_cards.all():
|
||||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||||
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
|
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
|
||||||
break
|
break
|
||||||
|
|
||||||
for m in position.granted_memberships.all():
|
for m in position.granted_memberships.all():
|
||||||
m.canceled = False
|
m.canceled = False
|
||||||
m.save()
|
m.save()
|
||||||
order.create_transactions()
|
order.create_transactions()
|
||||||
else:
|
else:
|
||||||
raise OrderError(is_available)
|
raise OrderError(is_available)
|
||||||
|
|
||||||
@@ -264,7 +269,6 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
|
|||||||
if new_date < now():
|
if new_date < now():
|
||||||
raise OrderError(_('The new expiry date needs to be in the future.'))
|
raise OrderError(_('The new expiry date needs to be in the future.'))
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def change(was_expired=True):
|
def change(was_expired=True):
|
||||||
old_date = order.expires
|
old_date = order.expires
|
||||||
order.expires = new_date
|
order.expires = new_date
|
||||||
@@ -302,11 +306,11 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
|
|||||||
generate_invoice(order)
|
generate_invoice(order)
|
||||||
order.create_transactions()
|
order.create_transactions()
|
||||||
|
|
||||||
if order.status == Order.STATUS_PENDING:
|
with transaction.atomic():
|
||||||
change(was_expired=False)
|
if order.status == Order.STATUS_PENDING:
|
||||||
else:
|
change(was_expired=False)
|
||||||
with order.event.lock() as now_dt:
|
else:
|
||||||
is_available = order._is_still_available(now_dt, count_waitinglist=False, force=force)
|
is_available = order._is_still_available(now(), count_waitinglist=False, lock=True, force=force)
|
||||||
if is_available is True:
|
if is_available is True:
|
||||||
change(was_expired=True)
|
change(was_expired=True)
|
||||||
else:
|
else:
|
||||||
@@ -334,9 +338,8 @@ def mark_order_expired(order, user=None, auth=None):
|
|||||||
order = Order.objects.get(pk=order)
|
order = Order.objects.get(pk=order)
|
||||||
if isinstance(user, int):
|
if isinstance(user, int):
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
with order.event.lock():
|
order.status = Order.STATUS_EXPIRED
|
||||||
order.status = Order.STATUS_EXPIRED
|
order.save(update_fields=['status'])
|
||||||
order.save(update_fields=['status'])
|
|
||||||
|
|
||||||
order.log_action('pretix.event.order.expired', user=user, auth=auth)
|
order.log_action('pretix.event.order.expired', user=user, auth=auth)
|
||||||
i = order.invoices.filter(is_cancellation=False).last()
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
@@ -439,9 +442,8 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
if not order.require_approval or not order.status == Order.STATUS_PENDING:
|
||||||
raise OrderError(_('This order is not pending approval.'))
|
raise OrderError(_('This order is not pending approval.'))
|
||||||
|
|
||||||
with order.event.lock():
|
order.status = Order.STATUS_CANCELED
|
||||||
order.status = Order.STATUS_CANCELED
|
order.save(update_fields=['status'])
|
||||||
order.save(update_fields=['status'])
|
|
||||||
|
|
||||||
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
|
order.log_action('pretix.event.order.denied', user=user, auth=auth, data={
|
||||||
'comment': comment
|
'comment': comment
|
||||||
@@ -521,51 +523,49 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
m.save()
|
m.save()
|
||||||
|
|
||||||
if cancellation_fee:
|
if cancellation_fee:
|
||||||
with order.event.lock():
|
for position in order.positions.all():
|
||||||
for position in order.positions.all():
|
if position.voucher:
|
||||||
if position.voucher:
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
position.canceled = True
|
||||||
position.canceled = True
|
assign_ticket_secret(
|
||||||
assign_ticket_secret(
|
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||||
event=order.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
)
|
||||||
)
|
position.save(update_fields=['canceled', 'secret'])
|
||||||
position.save(update_fields=['canceled', 'secret'])
|
new_fee = cancellation_fee
|
||||||
new_fee = cancellation_fee
|
for fee in order.fees.all():
|
||||||
for fee in order.fees.all():
|
if keep_fees and fee in keep_fees:
|
||||||
if keep_fees and fee in keep_fees:
|
new_fee -= fee.value
|
||||||
new_fee -= fee.value
|
|
||||||
else:
|
|
||||||
fee.canceled = True
|
|
||||||
fee.save(update_fields=['canceled'])
|
|
||||||
|
|
||||||
if new_fee:
|
|
||||||
f = OrderFee(
|
|
||||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
|
||||||
value=new_fee,
|
|
||||||
tax_rule=order.event.settings.tax_rate_default,
|
|
||||||
order=order,
|
|
||||||
)
|
|
||||||
f._calculate_tax()
|
|
||||||
f.save()
|
|
||||||
|
|
||||||
if cancellation_fee > order.total:
|
|
||||||
raise OrderError(_('The cancellation fee cannot be higher than the total amount of this order.'))
|
|
||||||
elif order.payment_refund_sum < cancellation_fee:
|
|
||||||
order.status = Order.STATUS_PENDING
|
|
||||||
order.set_expires()
|
|
||||||
else:
|
else:
|
||||||
order.status = Order.STATUS_PAID
|
fee.canceled = True
|
||||||
order.total = cancellation_fee
|
fee.save(update_fields=['canceled'])
|
||||||
order.cancellation_date = now()
|
|
||||||
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
if new_fee:
|
||||||
|
f = OrderFee(
|
||||||
|
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||||
|
value=new_fee,
|
||||||
|
tax_rule=order.event.settings.tax_rate_default,
|
||||||
|
order=order,
|
||||||
|
)
|
||||||
|
f._calculate_tax()
|
||||||
|
f.save()
|
||||||
|
|
||||||
|
if cancellation_fee > order.total:
|
||||||
|
raise OrderError(_('The cancellation fee cannot be higher than the total amount of this order.'))
|
||||||
|
elif order.payment_refund_sum < cancellation_fee:
|
||||||
|
order.status = Order.STATUS_PENDING
|
||||||
|
order.set_expires()
|
||||||
|
else:
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
order.total = cancellation_fee
|
||||||
|
order.cancellation_date = now()
|
||||||
|
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
||||||
|
|
||||||
if cancel_invoice and i:
|
if cancel_invoice and i:
|
||||||
invoices.append(generate_invoice(order))
|
invoices.append(generate_invoice(order))
|
||||||
else:
|
else:
|
||||||
with order.event.lock():
|
order.status = Order.STATUS_CANCELED
|
||||||
order.status = Order.STATUS_CANCELED
|
order.cancellation_date = now()
|
||||||
order.cancellation_date = now()
|
order.save(update_fields=['status', 'cancellation_date'])
|
||||||
order.save(update_fields=['status', 'cancellation_date'])
|
|
||||||
|
|
||||||
for position in order.positions.all():
|
for position in order.positions.all():
|
||||||
assign_ticket_secret(
|
assign_ticket_secret(
|
||||||
@@ -666,6 +666,27 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
|
|
||||||
sorted_positions = sorted(positions, key=lambda s: -int(s.is_bundled))
|
sorted_positions = sorted(positions, key=lambda s: -int(s.is_bundled))
|
||||||
|
|
||||||
|
for cp in sorted_positions:
|
||||||
|
cp._cached_quotas = list(cp.quotas)
|
||||||
|
|
||||||
|
# Create locks
|
||||||
|
if any(cp.expires < now() + timedelta(seconds=LOCK_TRUST_WINDOW) for cp in sorted_positions):
|
||||||
|
# No need to perform any locking if the cart positions still guarantee everything long enough.
|
||||||
|
full_lock_required = any(
|
||||||
|
getattr(o, 'seat', False) for o in sorted_positions
|
||||||
|
) and event.settings.seating_minimal_distance > 0
|
||||||
|
if full_lock_required:
|
||||||
|
# We lock the entire event in this case since we don't want to deal with fine-granular locking
|
||||||
|
# in the case of seating distance enforcement
|
||||||
|
lock_objects([event])
|
||||||
|
else:
|
||||||
|
lock_objects(
|
||||||
|
[q for q in reduce(operator.or_, (set(cp._cached_quotas) for cp in sorted_positions), set()) if q.size is not None] +
|
||||||
|
[op.voucher for op in sorted_positions if op.voucher] +
|
||||||
|
[op.seat for op in sorted_positions if op.seat],
|
||||||
|
shared_lock_objects=[event]
|
||||||
|
)
|
||||||
|
|
||||||
# Check availability
|
# Check availability
|
||||||
for i, cp in enumerate(sorted_positions):
|
for i, cp in enumerate(sorted_positions):
|
||||||
if cp.pk in deleted_positions:
|
if cp.pk in deleted_positions:
|
||||||
@@ -675,7 +696,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
quotas = list(cp.quotas)
|
quotas = cp._cached_quotas
|
||||||
|
|
||||||
products_seen[cp.item] += 1
|
products_seen[cp.item] += 1
|
||||||
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
|
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
|
||||||
@@ -689,6 +710,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
if cp.voucher:
|
if cp.voucher:
|
||||||
v_usages[cp.voucher] += 1
|
v_usages[cp.voucher] += 1
|
||||||
if cp.voucher not in v_avail:
|
if cp.voucher not in v_avail:
|
||||||
|
cp.voucher.refresh_from_db(fields=['redeemed'])
|
||||||
redeemed_in_carts = CartPosition.objects.filter(
|
redeemed_in_carts = CartPosition.objects.filter(
|
||||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||||
).exclude(cart_id=cp.cart_id)
|
).exclude(cart_id=cp.cart_id)
|
||||||
@@ -927,91 +949,87 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
payments = []
|
payments = []
|
||||||
sales_channel = get_all_sales_channels()[sales_channel]
|
sales_channel = get_all_sales_channels()[sales_channel]
|
||||||
|
|
||||||
with transaction.atomic():
|
try:
|
||||||
|
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise OrderError(e.message)
|
||||||
|
|
||||||
try:
|
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
|
||||||
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
try:
|
||||||
except ValidationError as e:
|
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||||
raise OrderError(e.message)
|
except TaxRule.SaleNotAllowed:
|
||||||
|
raise OrderError(error_messages['country_blocked'])
|
||||||
|
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||||
|
|
||||||
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
|
order = Order(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
event=event,
|
||||||
|
email=email,
|
||||||
|
phone=(meta_info or {}).get('contact_form_data', {}).get('phone'),
|
||||||
|
datetime=now_dt,
|
||||||
|
locale=get_language_without_region(locale),
|
||||||
|
total=total,
|
||||||
|
testmode=True if sales_channel.testmode_supported and event.testmode else False,
|
||||||
|
meta_info=json.dumps(meta_info or {}),
|
||||||
|
require_approval=require_approval,
|
||||||
|
sales_channel=sales_channel.identifier,
|
||||||
|
customer=customer,
|
||||||
|
valid_if_pending=valid_if_pending,
|
||||||
|
)
|
||||||
|
if customer:
|
||||||
|
order.email_known_to_work = customer.is_verified
|
||||||
|
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
if address:
|
||||||
|
if address.order is not None:
|
||||||
|
address.pk = None
|
||||||
|
address.order = order
|
||||||
|
address.save()
|
||||||
|
|
||||||
|
for fee in fees:
|
||||||
|
fee.order = order
|
||||||
try:
|
try:
|
||||||
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
fee._calculate_tax()
|
||||||
except TaxRule.SaleNotAllowed:
|
except TaxRule.SaleNotAllowed:
|
||||||
raise OrderError(error_messages['country_blocked'])
|
raise OrderError(error_messages['country_blocked'])
|
||||||
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
if fee.tax_rule and not fee.tax_rule.pk:
|
||||||
|
fee.tax_rule = None # TODO: deprecate
|
||||||
|
fee.save()
|
||||||
|
|
||||||
order = Order(
|
# Safety check: Is the amount we're now going to charge the same amount the user has been shown when they
|
||||||
status=Order.STATUS_PENDING,
|
# pressed "Confirm purchase"? If not, we should better warn the user and show the confirmation page again.
|
||||||
event=event,
|
# We used to have a *known* case where this happened is if a gift card is used in two concurrent sessions,
|
||||||
email=email,
|
# but this is now a payment error instead. So currently this code branch is usually only triggered by bugs
|
||||||
phone=(meta_info or {}).get('contact_form_data', {}).get('phone'),
|
# in other places (e.g. tax calculation).
|
||||||
datetime=now_dt,
|
if shown_total is not None:
|
||||||
locale=get_language_without_region(locale),
|
if Decimal(shown_total) != pending_sum:
|
||||||
total=total,
|
raise OrderError(
|
||||||
testmode=True if sales_channel.testmode_supported and event.testmode else False,
|
_('While trying to place your order, we noticed that the order total has changed. Either one of '
|
||||||
meta_info=json.dumps(meta_info or {}),
|
'the prices changed just now, or a gift card you used has been used in the meantime. Please '
|
||||||
require_approval=require_approval,
|
'check the prices below and try again.')
|
||||||
sales_channel=sales_channel.identifier,
|
)
|
||||||
customer=customer,
|
|
||||||
valid_if_pending=valid_if_pending,
|
|
||||||
)
|
|
||||||
if customer:
|
|
||||||
order.email_known_to_work = customer.is_verified
|
|
||||||
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
if address:
|
if payment_requests and not order.require_approval:
|
||||||
if address.order is not None:
|
for p in payment_requests:
|
||||||
address.pk = None
|
if not p.get('multi_use_supported') or p['payment_amount'] > Decimal('0.00'):
|
||||||
address.order = order
|
payments.append(order.payments.create(
|
||||||
address.save()
|
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||||
|
provider=p['provider'],
|
||||||
|
amount=p['payment_amount'],
|
||||||
|
fee=p.get('fee'),
|
||||||
|
info=json.dumps(p['info_data']),
|
||||||
|
process_initiated=False,
|
||||||
|
))
|
||||||
|
|
||||||
order.save()
|
orderpositions = OrderPosition.transform_cart_positions(positions, order)
|
||||||
|
order.create_transactions(positions=orderpositions, fees=fees, is_new=True)
|
||||||
for fee in fees:
|
order.log_action('pretix.event.order.placed')
|
||||||
fee.order = order
|
if order.require_approval:
|
||||||
try:
|
order.log_action('pretix.event.order.placed.require_approval')
|
||||||
fee._calculate_tax()
|
if meta_info:
|
||||||
except TaxRule.SaleNotAllowed:
|
for msg in meta_info.get('confirm_messages', []):
|
||||||
raise OrderError(error_messages['country_blocked'])
|
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||||
if fee.tax_rule and not fee.tax_rule.pk:
|
|
||||||
fee.tax_rule = None # TODO: deprecate
|
|
||||||
fee.save()
|
|
||||||
|
|
||||||
# Safety check: Is the amount we're now going to charge the same amount the user has been shown when they
|
|
||||||
# pressed "Confirm purchase"? If not, we should better warn the user and show the confirmation page again.
|
|
||||||
# We used to have a *known* case where this happened is if a gift card is used in two concurrent sessions,
|
|
||||||
# but this is now a payment error instead. So currently this code branch is usually only triggered by bugs
|
|
||||||
# in other places (e.g. tax calculation).
|
|
||||||
if shown_total is not None:
|
|
||||||
if Decimal(shown_total) != pending_sum:
|
|
||||||
raise OrderError(
|
|
||||||
_('While trying to place your order, we noticed that the order total has changed. Either one of '
|
|
||||||
'the prices changed just now, or a gift card you used has been used in the meantime. Please '
|
|
||||||
'check the prices below and try again.')
|
|
||||||
)
|
|
||||||
|
|
||||||
if payment_requests and not order.require_approval:
|
|
||||||
for p in payment_requests:
|
|
||||||
if not p.get('multi_use_supported') or p['payment_amount'] > Decimal('0.00'):
|
|
||||||
payments.append(order.payments.create(
|
|
||||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
|
||||||
provider=p['provider'],
|
|
||||||
amount=p['payment_amount'],
|
|
||||||
fee=p.get('fee'),
|
|
||||||
info=json.dumps(p['info_data']),
|
|
||||||
process_initiated=False,
|
|
||||||
))
|
|
||||||
|
|
||||||
orderpositions = OrderPosition.transform_cart_positions(positions, order)
|
|
||||||
order.create_transactions(positions=orderpositions, fees=fees, is_new=True)
|
|
||||||
order.log_action('pretix.event.order.placed')
|
|
||||||
if order.require_approval:
|
|
||||||
order.log_action('pretix.event.order.placed.require_approval')
|
|
||||||
if meta_info:
|
|
||||||
for msg in meta_info.get('confirm_messages', []):
|
|
||||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
|
||||||
|
|
||||||
order_placed.send(event, order=order)
|
order_placed.send(event, order=order)
|
||||||
return order, payments
|
return order, payments
|
||||||
@@ -1116,18 +1134,12 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
if result:
|
if result:
|
||||||
valid_if_pending = True
|
valid_if_pending = True
|
||||||
|
|
||||||
lockfn = NoLockManager
|
|
||||||
locked = False
|
|
||||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
|
|
||||||
# Performance optimization: If no voucher is used and no cart position is dangerously close to its expiry date,
|
|
||||||
# creating this order shouldn't be prone to any race conditions and we don't need to lock the event.
|
|
||||||
locked = True
|
|
||||||
lockfn = event.lock
|
|
||||||
|
|
||||||
warnings = []
|
warnings = []
|
||||||
any_payment_failed = False
|
any_payment_failed = False
|
||||||
|
|
||||||
with lockfn() as now_dt:
|
now_dt = now()
|
||||||
|
err_out = None
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
positions = list(
|
positions = list(
|
||||||
positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
|
positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
|
||||||
)
|
)
|
||||||
@@ -1136,16 +1148,28 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
raise OrderError(error_messages['empty'])
|
raise OrderError(error_messages['empty'])
|
||||||
if len(position_ids) != len(positions):
|
if len(position_ids) != len(positions):
|
||||||
raise OrderError(error_messages['internal'])
|
raise OrderError(error_messages['internal'])
|
||||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
|
||||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
|
||||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
|
||||||
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
|
||||||
try:
|
try:
|
||||||
for p in payment_objs:
|
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
||||||
if p.provider == 'free':
|
except OrderError as e:
|
||||||
p.confirm(send_mail=False, lock=not locked, generate_invoice=False)
|
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
|
||||||
except Quota.QuotaExceededException:
|
else:
|
||||||
pass
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
||||||
|
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||||
|
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for p in payment_objs:
|
||||||
|
if p.provider == 'free':
|
||||||
|
# Passing lock=False is safe here because it's absolutely impossible for the order to be expired
|
||||||
|
# here before it is even committed.
|
||||||
|
p.confirm(send_mail=False, lock=False, generate_invoice=False)
|
||||||
|
except Quota.QuotaExceededException:
|
||||||
|
pass
|
||||||
|
if err_out:
|
||||||
|
raise err_out
|
||||||
|
|
||||||
# We give special treatment to GiftCardPayment here because our invoice renderer expects gift cards to already be
|
# We give special treatment to GiftCardPayment here because our invoice renderer expects gift cards to already be
|
||||||
# processed, and because we historically treat gift card orders like free orders with regards to email texts.
|
# processed, and because we historically treat gift card orders like free orders with regards to email texts.
|
||||||
@@ -1289,9 +1313,19 @@ def send_expiry_warnings(sender, **kwargs):
|
|||||||
event_id = None
|
event_id = None
|
||||||
|
|
||||||
for o in Order.objects.filter(
|
for o in Order.objects.filter(
|
||||||
expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING,
|
expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING,
|
||||||
datetime__lte=now() - timedelta(hours=2), require_approval=False
|
datetime__lte=now() - timedelta(hours=2), require_approval=False
|
||||||
).only('pk', 'event_id', 'expires').order_by('event_id'):
|
).only('pk', 'event_id', 'expires').order_by('event_id'):
|
||||||
|
|
||||||
|
lp = o.payments.last()
|
||||||
|
if (
|
||||||
|
lp and
|
||||||
|
lp.state in [OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING] and
|
||||||
|
lp.payment_provider and
|
||||||
|
lp.payment_provider.prevent_reminder_mail(o, lp)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
if event_id != o.event_id:
|
if event_id != o.event_id:
|
||||||
settings = o.event.settings
|
settings = o.event.settings
|
||||||
days = cache.get_or_set('{}:{}:setting_mail_days_order_expire_warning'.format('event', o.event_id),
|
days = cache.get_or_set('{}:{}:setting_mail_days_order_expire_warning'.format('event', o.event_id),
|
||||||
@@ -2648,6 +2682,19 @@ class OrderChangeManager:
|
|||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise OrderError(e.message)
|
raise OrderError(e.message)
|
||||||
|
|
||||||
|
def _create_locks(self):
|
||||||
|
full_lock_required = any(diff > 0 for diff in self._seatdiff.values()) and self.event.settings.seating_minimal_distance > 0
|
||||||
|
if full_lock_required:
|
||||||
|
# We lock the entire event in this case since we don't want to deal with fine-granular locking
|
||||||
|
# in the case of seating distance enforcement
|
||||||
|
lock_objects([self.event])
|
||||||
|
else:
|
||||||
|
lock_objects(
|
||||||
|
[q for q, d in self._quotadiff.items() if q.size is not None and d > 0] +
|
||||||
|
[s for s, d in self._seatdiff.items() if d > 0],
|
||||||
|
shared_lock_objects=[self.event]
|
||||||
|
)
|
||||||
|
|
||||||
def commit(self, check_quotas=True):
|
def commit(self, check_quotas=True):
|
||||||
if self._committed:
|
if self._committed:
|
||||||
# an order change can only be committed once
|
# an order change can only be committed once
|
||||||
@@ -2667,17 +2714,17 @@ class OrderChangeManager:
|
|||||||
self._payment_fee_diff()
|
self._payment_fee_diff()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
with self.order.event.lock():
|
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_PAID):
|
||||||
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_PAID):
|
if check_quotas:
|
||||||
if check_quotas:
|
self._check_quotas()
|
||||||
self._check_quotas()
|
self._check_seats()
|
||||||
self._check_seats()
|
self._create_locks()
|
||||||
self._check_complete_cancel()
|
self._check_complete_cancel()
|
||||||
self._check_and_lock_memberships()
|
self._check_and_lock_memberships()
|
||||||
try:
|
try:
|
||||||
self._perform_operations()
|
self._perform_operations()
|
||||||
except TaxRule.SaleNotAllowed:
|
except TaxRule.SaleNotAllowed:
|
||||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||||
self._recalculate_total_and_payment_fee()
|
self._recalculate_total_and_payment_fee()
|
||||||
self._check_paid_price_change()
|
self._check_paid_price_change()
|
||||||
self._check_paid_to_free()
|
self._check_paid_to_free()
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class QuotaAvailability:
|
|||||||
update[q.event_id].append(q)
|
update[q.event_id].append(q)
|
||||||
|
|
||||||
for eventid, quotas in update.items():
|
for eventid, quotas in update.items():
|
||||||
rc.hmset(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', {
|
rc.hset(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', mapping={
|
||||||
str(q.id): ",".join(
|
str(q.id): ",".join(
|
||||||
[str(i) for i in self.results[q]] +
|
[str(i) for i in self.results[q]] +
|
||||||
[str(int(time.time()))]
|
[str(int(time.time()))]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Exists, F, OuterRef, Prefetch, Q, Sum, prefetch_related_objects,
|
Exists, F, OuterRef, Prefetch, Q, Sum, prefetch_related_objects,
|
||||||
)
|
)
|
||||||
@@ -33,6 +34,7 @@ from pretix.base.models import (
|
|||||||
Event, EventMetaValue, SeatCategoryMapping, User, WaitingListEntry,
|
Event, EventMetaValue, SeatCategoryMapping, User, WaitingListEntry,
|
||||||
)
|
)
|
||||||
from pretix.base.models.waitinglist import WaitingListException
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
|
from pretix.base.services.locking import lock_objects
|
||||||
from pretix.base.services.tasks import EventTask
|
from pretix.base.services.tasks import EventTask
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
@@ -86,11 +88,23 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
|
|
||||||
sent = 0
|
sent = 0
|
||||||
|
|
||||||
with event.lock():
|
with transaction.atomic(durable=True):
|
||||||
|
quotas_by_item = {}
|
||||||
|
quotas = set()
|
||||||
|
for wle in qs:
|
||||||
|
if (wle.item_id, wle.variation_id, wle.subevent_id) not in quotas_by_item:
|
||||||
|
quotas_by_item[wle.item_id, wle.variation_id, wle.subevent_id] = list(
|
||||||
|
wle.variation.quotas.filter(subevent=wle.subevent)
|
||||||
|
if wle.variation
|
||||||
|
else wle.item.quotas.filter(subevent=wle.subevent)
|
||||||
|
)
|
||||||
|
wle._quotas = quotas_by_item[wle.item_id, wle.variation_id, wle.subevent_id]
|
||||||
|
quotas |= set(wle._quotas)
|
||||||
|
|
||||||
|
lock_objects(quotas, shared_lock_objects=[event])
|
||||||
for wle in qs:
|
for wle in qs:
|
||||||
if (wle.item, wle.variation, wle.subevent) in gone:
|
if (wle.item, wle.variation, wle.subevent) in gone:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ev = (wle.subevent or event)
|
ev = (wle.subevent or event)
|
||||||
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
||||||
continue
|
continue
|
||||||
@@ -105,9 +119,6 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
gone.add((wle.item, wle.variation, wle.subevent))
|
gone.add((wle.item, wle.variation, wle.subevent))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
|
||||||
if wle.variation
|
|
||||||
else wle.item.quotas.filter(subevent=wle.subevent))
|
|
||||||
availability = (
|
availability = (
|
||||||
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
|
||||||
if wle.variation
|
if wle.variation
|
||||||
@@ -121,7 +132,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Reduce affected quotas in cache
|
# Reduce affected quotas in cache
|
||||||
for q in quotas:
|
for q in wle._quotas:
|
||||||
quota_cache[q.pk] = (
|
quota_cache[q.pk] = (
|
||||||
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
|
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
|
||||||
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
||||||
|
|||||||
@@ -1033,10 +1033,10 @@ DEFAULTS = {
|
|||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
choices=(
|
choices=(
|
||||||
('False', _('Do not generate invoices')),
|
('False', _('Do not generate invoices')),
|
||||||
('admin', _('Only manually in admin panel')),
|
('paid', _('Automatically after payment or when required by payment method')),
|
||||||
|
('True', _('Automatically before payment for all created orders')),
|
||||||
('user', _('Automatically on user request')),
|
('user', _('Automatically on user request')),
|
||||||
('True', _('Automatically for all created orders')),
|
('admin', _('Only manually in admin panel')),
|
||||||
('paid', _('Automatically on payment or when required by payment method')),
|
|
||||||
),
|
),
|
||||||
help_text=_("Invoices will never be automatically generated for free orders.")
|
help_text=_("Invoices will never be automatically generated for free orders.")
|
||||||
)
|
)
|
||||||
@@ -2793,7 +2793,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||||
@@ -2836,7 +2836,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||||
@@ -2876,7 +2876,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Social media image'),
|
label=_('Social media image'),
|
||||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||||
@@ -2897,7 +2897,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Logo image'),
|
label=_('Logo image'),
|
||||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
required=False,
|
required=False,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ class SerializerDateFrameField(serializers.CharField):
|
|||||||
resolve_timeframe_to_dates_inclusive(now(), data, timezone.utc)
|
resolve_timeframe_to_dates_inclusive(now(), data, timezone.utc)
|
||||||
except:
|
except:
|
||||||
raise ValidationError("Invalid date frame")
|
raise ValidationError("Invalid date frame")
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from celery.result import AsyncResult
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpResponse, JsonResponse, QueryDict
|
from django.http import HttpResponse, JsonResponse, QueryDict
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
@@ -217,6 +218,7 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
known_errortypes = ['ValidationError']
|
known_errortypes = ['ValidationError']
|
||||||
expected_exceptions = (ValidationError,)
|
expected_exceptions = (ValidationError,)
|
||||||
task_base = ProfiledEventTask
|
task_base = ProfiledEventTask
|
||||||
|
atomic_execute = False
|
||||||
|
|
||||||
def async_set_progress(self, percentage):
|
def async_set_progress(self, percentage):
|
||||||
if not self._task_self.request.called_directly:
|
if not self._task_self.request.called_directly:
|
||||||
@@ -263,6 +265,9 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
form.is_valid()
|
form.is_valid()
|
||||||
return view_instance.async_form_valid(self, form)
|
return view_instance.async_form_valid(self, form)
|
||||||
|
|
||||||
|
if cls.atomic_execute:
|
||||||
|
async_execute = transaction.atomic(async_execute)
|
||||||
|
|
||||||
cls.async_execute = app.task(
|
cls.async_execute = app.task(
|
||||||
base=cls.task_base,
|
base=cls.task_base,
|
||||||
bind=True,
|
bind=True,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from django.forms.utils import from_current_timezone
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.text import format_lazy
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
@@ -51,6 +52,7 @@ from django_scopes.forms import SafeModelMultipleChoiceField
|
|||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
|
|
||||||
from ...base.forms import I18nModelForm
|
from ...base.forms import I18nModelForm
|
||||||
|
from ...helpers.i18n import get_language_score
|
||||||
from ...helpers.images import (
|
from ...helpers.images import (
|
||||||
IMAGE_EXTS, validate_uploaded_file_for_valid_image,
|
IMAGE_EXTS, validate_uploaded_file_for_valid_image,
|
||||||
)
|
)
|
||||||
@@ -127,7 +129,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_img(self):
|
def is_img(self):
|
||||||
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
|
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_IMAGE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if hasattr(self.file, 'display_name'):
|
if hasattr(self.file, 'display_name'):
|
||||||
@@ -300,18 +302,44 @@ class SlugWidget(forms.TextInput):
|
|||||||
|
|
||||||
|
|
||||||
class MultipleLanguagesWidget(forms.CheckboxSelectMultiple):
|
class MultipleLanguagesWidget(forms.CheckboxSelectMultiple):
|
||||||
|
template_name = 'pretixcontrol/multi_languages_select.html'
|
||||||
option_template_name = 'pretixcontrol/multi_languages_widget.html'
|
option_template_name = 'pretixcontrol/multi_languages_widget.html'
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
self.choices = sorted(self.choices, key=lambda l: (
|
def filter_and_sort(choices, languages, cond=True):
|
||||||
|
return sorted(
|
||||||
|
[c for c in choices if (c[0] in languages) == cond],
|
||||||
|
key=lambda c: str(c[1])
|
||||||
|
)
|
||||||
|
self.choices = (
|
||||||
(
|
(
|
||||||
0 if l[0] in settings.LANGUAGES_OFFICIAL
|
'',
|
||||||
else (
|
filter_and_sort(self.choices, settings.LANGUAGES_OFFICIAL)
|
||||||
1 if l[0] not in settings.LANGUAGES_INCUBATING
|
),
|
||||||
else 2
|
(
|
||||||
)
|
(
|
||||||
), str(l[1])
|
_('Community translations'),
|
||||||
))
|
format_lazy(
|
||||||
|
_('These translations are not maintained by the pretix team. We cannot vouch for their correctness '
|
||||||
|
'and new or recently changed features might not be translated and will show in English instead. '
|
||||||
|
'You can <a href="{translate_url}" target="_blank">help translating</a>.'),
|
||||||
|
translate_url='https://translate.pretix.eu'
|
||||||
|
),
|
||||||
|
'fa fa-group'
|
||||||
|
),
|
||||||
|
filter_and_sort(self.choices, settings.LANGUAGES_OFFICIAL.union(settings.LANGUAGES_INCUBATING), False)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(
|
||||||
|
_('Development only'),
|
||||||
|
_('These translations are still in progress. These languages can currently only be selected on development '
|
||||||
|
'installations of pretix, not in production.'),
|
||||||
|
'fa fa-flask text-danger'
|
||||||
|
),
|
||||||
|
filter_and_sort(self.choices, settings.LANGUAGES_INCUBATING)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.choices = [c for c in self.choices if len(c[1])]
|
||||||
|
|
||||||
def options(self, name, value, attrs=None):
|
def options(self, name, value, attrs=None):
|
||||||
self.sort()
|
self.sort()
|
||||||
@@ -325,6 +353,8 @@ class MultipleLanguagesWidget(forms.CheckboxSelectMultiple):
|
|||||||
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||||
opt['official'] = value in settings.LANGUAGES_OFFICIAL
|
opt['official'] = value in settings.LANGUAGES_OFFICIAL
|
||||||
opt['incubating'] = value in settings.LANGUAGES_INCUBATING
|
opt['incubating'] = value in settings.LANGUAGES_INCUBATING
|
||||||
|
base_score = get_language_score("de")
|
||||||
|
opt['score'] = round(get_language_score(value) / base_score * 100)
|
||||||
return opt
|
return opt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from django_scopes.forms import (
|
|||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import ItemMultipleChoiceField
|
from pretix.control.forms import ItemMultipleChoiceField
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
@@ -201,3 +202,26 @@ class CheckinListSimulatorForm(forms.Form):
|
|||||||
initial=True,
|
initial=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
gate = SafeModelChoiceField(
|
||||||
|
label=_('Gate'),
|
||||||
|
empty_label=_('All gates'),
|
||||||
|
queryset=Gate.objects.none(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
||||||
|
self.fields['gate'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:organizer.gates.select2', kwargs={
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': _('All gates'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['gate'].widget.choices = self.fields['gate'].choices
|
||||||
|
self.fields['gate'].label = _('Gate')
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from django.forms import (
|
|||||||
)
|
)
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||||
@@ -62,11 +62,12 @@ from pytz import common_timezones
|
|||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import (
|
||||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
|
||||||
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||||
)
|
)
|
||||||
from pretix.base.validators import multimail_validate
|
from pretix.base.validators import multimail_validate
|
||||||
@@ -900,6 +901,27 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
)
|
)
|
||||||
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
||||||
|
|
||||||
|
pps = [str(pp.verbose_name) for pp in event.get_payment_providers().values() if pp.requires_invoice_immediately]
|
||||||
|
if pps:
|
||||||
|
generate_paid_help_text = _('An invoice will be issued before payment if the customer selects one of the following payment methods: {list}').format(
|
||||||
|
list=', '.join(pps)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
generate_paid_help_text = _('None of the currently configured payment methods will cause an invoice to be issued before payment.')
|
||||||
|
|
||||||
|
generate_choices = list(DEFAULTS['invoice_generate']['form_kwargs']['choices'])
|
||||||
|
idx = [i for i, t in enumerate(generate_choices) if t[0] == 'paid'][0]
|
||||||
|
generate_choices[idx] = (
|
||||||
|
'paid',
|
||||||
|
format_html(
|
||||||
|
'{} <span class="label label-success">{}</span><br><span class="text-muted">{}</span>',
|
||||||
|
generate_choices[idx][1],
|
||||||
|
_('Recommended'),
|
||||||
|
generate_paid_help_text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.fields['invoice_generate'].choices = generate_choices
|
||||||
|
|
||||||
|
|
||||||
def contains_web_channel_validate(val):
|
def contains_web_channel_validate(val):
|
||||||
if "web" not in val:
|
if "web" not in val:
|
||||||
@@ -1275,12 +1297,12 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_subject_order_placed_require_approval': ['event', 'order'],
|
'mail_subject_order_placed_require_approval': ['event', 'order'],
|
||||||
'mail_text_order_approved': ['event', 'order'],
|
'mail_text_order_approved': ['event', 'order'],
|
||||||
'mail_subject_order_approved': ['event', 'order'],
|
'mail_subject_order_approved': ['event', 'order'],
|
||||||
'mail_text_order_approved_attendee': ['event', 'order'],
|
'mail_text_order_approved_attendee': ['event', 'order', 'position'],
|
||||||
'mail_subject_order_approved_attendee': ['event', 'order'],
|
'mail_subject_order_approved_attendee': ['event', 'order', 'position'],
|
||||||
'mail_text_order_approved_free': ['event', 'order'],
|
'mail_text_order_approved_free': ['event', 'order'],
|
||||||
'mail_subject_order_approved_free': ['event', 'order'],
|
'mail_subject_order_approved_free': ['event', 'order'],
|
||||||
'mail_text_order_approved_free_attendee': ['event', 'order'],
|
'mail_text_order_approved_free_attendee': ['event', 'order', 'position'],
|
||||||
'mail_subject_order_approved_free_attendee': ['event', 'order'],
|
'mail_subject_order_approved_free_attendee': ['event', 'order', 'position'],
|
||||||
'mail_text_order_denied': ['event', 'order', 'comment'],
|
'mail_text_order_denied': ['event', 'order', 'comment'],
|
||||||
'mail_subject_order_denied': ['event', 'order', 'comment'],
|
'mail_subject_order_denied': ['event', 'order', 'comment'],
|
||||||
'mail_text_order_paid': ['event', 'order', 'payment_info'],
|
'mail_text_order_paid': ['event', 'order', 'payment_info'],
|
||||||
@@ -1310,7 +1332,7 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_subject_download_reminder_attendee': ['event', 'order', 'position'],
|
'mail_subject_download_reminder_attendee': ['event', 'order', 'position'],
|
||||||
'mail_text_resend_link': ['event', 'order'],
|
'mail_text_resend_link': ['event', 'order'],
|
||||||
'mail_subject_resend_link': ['event', 'order'],
|
'mail_subject_resend_link': ['event', 'order'],
|
||||||
'mail_subject_resend_link_attendee': ['event', 'order'],
|
'mail_subject_resend_link_attendee': ['event', 'order', 'position'],
|
||||||
'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
|
'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
|
||||||
'mail_subject_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
|
'mail_subject_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
|
||||||
'mail_text_resend_all_links': ['event', 'orders'],
|
'mail_text_resend_all_links': ['event', 'orders'],
|
||||||
@@ -1319,19 +1341,14 @@ class MailSettingsForm(SettingsForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
phs = [
|
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||||
'{%s}' % p
|
ht = format_placeholders_help_text(placeholders, self.event)
|
||||||
for p in sorted(get_available_placeholders(self.event, base_parameters).keys())
|
|
||||||
]
|
|
||||||
ht = _('Available placeholders: {list}').format(
|
|
||||||
list=', '.join(phs)
|
|
||||||
)
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
else:
|
else:
|
||||||
self.fields[fn].help_text = ht
|
self.fields[fn].help_text = ht
|
||||||
self.fields[fn].validators.append(
|
self.fields[fn].validators.append(
|
||||||
PlaceholderValidator(phs)
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -2397,6 +2397,61 @@ class CheckinFilterForm(FilterForm):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinListFilterForm(FilterForm):
|
||||||
|
orders = {
|
||||||
|
"name": ("name", "subevent__date_from", "pk"),
|
||||||
|
"-name": ("-name", "-subevent__date_from", "-pk"),
|
||||||
|
"subevent": ("subevent__date_from", "name", "pk"),
|
||||||
|
"-subevent": ("-subevent__date_from", "-name", "-pk"),
|
||||||
|
}
|
||||||
|
subevent = forms.ModelChoiceField(
|
||||||
|
label=pgettext_lazy('subevent', 'Date'),
|
||||||
|
queryset=SubEvent.objects.none(),
|
||||||
|
required=False,
|
||||||
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop("event")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.event.has_subevents:
|
||||||
|
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||||
|
self.fields['subevent'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||||
|
elif 'subevent':
|
||||||
|
del self.fields['subevent']
|
||||||
|
|
||||||
|
def filter_qs(self, qs):
|
||||||
|
fdata = self.cleaned_data
|
||||||
|
|
||||||
|
if fdata.get('subevent'):
|
||||||
|
qs = qs.filter(subevent=fdata.get('subevent'))
|
||||||
|
|
||||||
|
if fdata.get("ordering"):
|
||||||
|
ob = self.orders[fdata.get('ordering')]
|
||||||
|
if isinstance(ob, dict):
|
||||||
|
ob = dict(ob)
|
||||||
|
o = ob.pop('_order')
|
||||||
|
qs = qs.annotate(**ob).order_by(*get_deterministic_ordering(OrderPosition, [o]))
|
||||||
|
elif isinstance(ob, (list, tuple)):
|
||||||
|
qs = qs.order_by(*get_deterministic_ordering(OrderPosition, ob))
|
||||||
|
else:
|
||||||
|
qs = qs.order_by(*get_deterministic_ordering(OrderPosition, [ob]))
|
||||||
|
else:
|
||||||
|
qs = qs.order_by("subevent__date_from", "name", "pk")
|
||||||
|
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(FilterForm):
|
class DeviceFilterForm(FilterForm):
|
||||||
orders = {
|
orders = {
|
||||||
'name': Upper('name'),
|
'name': Upper('name'),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ from pretix.base.email import get_available_placeholders
|
|||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||||
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
|
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
DatePickerWidget, SplitDateTimePickerWidget,
|
DatePickerWidget, SplitDateTimePickerWidget, format_placeholders_help_text,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Invoice, InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
|
Invoice, InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
|
||||||
@@ -159,7 +159,7 @@ class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CancelForm(ForceQuotaConfirmationForm):
|
class CancelForm(forms.Form):
|
||||||
send_email = forms.BooleanField(
|
send_email = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Notify customer by email'),
|
label=_('Notify customer by email'),
|
||||||
@@ -188,6 +188,7 @@ class CancelForm(ForceQuotaConfirmationForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.instance = kwargs.pop("instance")
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
|
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
|
||||||
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
|
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
|
||||||
@@ -205,6 +206,20 @@ class CancelForm(ForceQuotaConfirmationForm):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
class DenyForm(forms.Form):
|
||||||
|
send_email = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
label=_('Notify customer by email'),
|
||||||
|
initial=True
|
||||||
|
)
|
||||||
|
comment = forms.CharField(
|
||||||
|
label=_('Comment (will be sent to the user)'),
|
||||||
|
help_text=_('Will be included in the notification email when the respective placeholder is present in the '
|
||||||
|
'configured email text.'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MarkPaidForm(ConfirmPaymentForm):
|
class MarkPaidForm(ConfirmPaymentForm):
|
||||||
send_email = forms.BooleanField(
|
send_email = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -662,19 +677,14 @@ class OrderMailForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
phs = [
|
placeholders = get_available_placeholders(self.order.event, base_parameters)
|
||||||
'{%s}' % p
|
ht = format_placeholders_help_text(placeholders, self.order.event)
|
||||||
for p in sorted(get_available_placeholders(self.order.event, base_parameters).keys())
|
|
||||||
]
|
|
||||||
ht = _('Available placeholders: {list}').format(
|
|
||||||
list=', '.join(phs)
|
|
||||||
)
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
else:
|
else:
|
||||||
self.fields[fn].help_text = ht
|
self.fields[fn].help_text = ht
|
||||||
self.fields[fn].validators.append(
|
self.fields[fn].validators.append(
|
||||||
PlaceholderValidator(phs)
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -857,19 +867,14 @@ class EventCancelForm(forms.Form):
|
|||||||
send_waitinglist_message = forms.CharField()
|
send_waitinglist_message = forms.CharField()
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
phs = [
|
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||||
'{%s}' % p
|
ht = format_placeholders_help_text(placeholders, self.event)
|
||||||
for p in sorted(get_available_placeholders(self.event, base_parameters).keys())
|
|
||||||
]
|
|
||||||
ht = _('Available placeholders: {list}').format(
|
|
||||||
list=', '.join(phs)
|
|
||||||
)
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
else:
|
else:
|
||||||
self.fields[fn].help_text = ht
|
self.fields[fn].help_text = ht
|
||||||
self.fields[fn].validators.append(
|
self.fields[fn].validators.append(
|
||||||
PlaceholderValidator(phs)
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ from pretix.base.forms.questions import (
|
|||||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
||||||
get_phone_prefix,
|
get_phone_prefix,
|
||||||
)
|
)
|
||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import (
|
||||||
|
SplitDateTimePickerWidget, format_placeholders_help_text,
|
||||||
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||||
@@ -420,7 +422,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
|
|
||||||
organizer_logo_image = ExtFileField(
|
organizer_logo_image = ExtFileField(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||||
@@ -430,7 +432,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
favicon = ExtFileField(
|
favicon = ExtFileField(
|
||||||
label=_('Favicon'),
|
label=_('Favicon'),
|
||||||
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||||
required=False,
|
required=False,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||||
@@ -568,19 +570,14 @@ class MailSettingsForm(SettingsForm):
|
|||||||
return placeholders
|
return placeholders
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
phs = [
|
placeholders = self._get_sample_context(base_parameters)
|
||||||
'{%s}' % p
|
ht = format_placeholders_help_text(placeholders)
|
||||||
for p in sorted(self._get_sample_context(base_parameters).keys())
|
|
||||||
]
|
|
||||||
ht = _('Available placeholders: {list}').format(
|
|
||||||
list=', '.join(phs)
|
|
||||||
)
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
else:
|
else:
|
||||||
self.fields[fn].help_text = ht
|
self.fields[fn].help_text = ht
|
||||||
self.fields[fn].validators.append(
|
self.fields[fn].validators.append(
|
||||||
PlaceholderValidator(phs)
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from django_scopes.forms import SafeModelChoiceField
|
|||||||
|
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||||
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
from pretix.base.models import Item, Voucher
|
from pretix.base.models import Item, Voucher
|
||||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
@@ -289,19 +290,15 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
Recipient = namedtuple('Recipient', 'email number name tag')
|
Recipient = namedtuple('Recipient', 'email number name tag')
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
phs = [
|
placeholders = get_available_placeholders(self.instance.event, base_parameters)
|
||||||
'{%s}' % p
|
ht = format_placeholders_help_text(placeholders, self.instance.event)
|
||||||
for p in sorted(get_available_placeholders(self.instance.event, base_parameters).keys())
|
|
||||||
]
|
|
||||||
ht = _('Available placeholders: {list}').format(
|
|
||||||
list=', '.join(phs)
|
|
||||||
)
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
else:
|
else:
|
||||||
self.fields[fn].help_text = ht
|
self.fields[fn].help_text = ht
|
||||||
self.fields[fn].validators.append(
|
self.fields[fn].validators.append(
|
||||||
PlaceholderValidator(phs)
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
id="product-select2">{% url "control:event.items.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
id="product-select2">{% url "control:event.items.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
||||||
<script type="text/plain"
|
<script type="text/plain"
|
||||||
id="variations-select2">{% url "control:event.items.variations.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
id="variations-select2">{% url "control:event.items.variations.select2" event=request.event.slug organizer=request.organizer.slug %}</script>
|
||||||
|
<script type="text/plain"
|
||||||
|
id="gates-select2">{% url "control:organizer.gates.select2" organizer=request.organizer.slug %}</script>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
<div class="tabbed-form">
|
<div class="tabbed-form">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{% extends "pretixcontrol/items/base.html" %}
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load urlreplace %}
|
||||||
{% block title %}{% trans "Check-in lists" %}{% endblock %}
|
{% block title %}{% trans "Check-in lists" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Check-in lists" %}</h1>
|
<h1>{% trans "Check-in lists" %}</h1>
|
||||||
@@ -27,11 +29,26 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
<div class="panel panel-default">
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
<div class="panel-heading">
|
||||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||||
|
</div>
|
||||||
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
|
<div class="row">
|
||||||
|
{% if filter_form.subevent %}
|
||||||
|
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.subevent %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if checkinlists|length == 0 %}
|
{% if checkinlists|length == 0 %}
|
||||||
<div class="empty-collection">
|
<div class="empty-collection">
|
||||||
@@ -47,7 +64,8 @@
|
|||||||
|
|
||||||
{% if "can_change_event_settings" in request.eventpermset %}
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}</a>
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_change_organizer_settings %}
|
{% if can_change_organizer_settings %}
|
||||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
||||||
@@ -69,10 +87,18 @@
|
|||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Check-in lists" %}</th>
|
<th>
|
||||||
|
{% trans "Name" %}
|
||||||
|
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
|
</th>
|
||||||
<th>{% trans "Checked in" %}</th>
|
<th>{% trans "Checked in" %}</th>
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<th>{% trans "Date" context "subevent" %}</th>
|
<th>
|
||||||
|
{% trans "Date" context "subevent" %}
|
||||||
|
<a href="?{% url_replace request 'ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
||||||
<th>{% trans "Products" %}</th>
|
<th>{% trans "Products" %}</th>
|
||||||
@@ -83,7 +109,8 @@
|
|||||||
{% for cl in checkinlists %}
|
{% for cl in checkinlists %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong><a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
<strong><a
|
||||||
|
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="quotabox availability">
|
<div class="quotabox availability">
|
||||||
@@ -92,14 +119,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="numbers">
|
<div class="numbers">
|
||||||
{{ cl.checkin_count|default_if_none:"0" }} / {{ cl.position_count|default_if_none:"0" }}
|
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||||
|
{{ cl.position_count|default_if_none:"0" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
{% if cl.subevent %}
|
{% if cl.subevent %}
|
||||||
<td>
|
<td>
|
||||||
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }} {{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}
|
||||||
|
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>
|
<td>
|
||||||
@@ -127,17 +156,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||||
{% if "can_change_event_settings" in request.eventpermset %}
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
||||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||||
<span class="fa fa-copy"></span>
|
<span class="fa fa-copy"></span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
{% bootstrap_field form.raw_barcode layout="control" %}
|
{% bootstrap_field form.raw_barcode layout="control" %}
|
||||||
{% bootstrap_field form.datetime layout="control" %}
|
{% bootstrap_field form.datetime layout="control" %}
|
||||||
{% bootstrap_field form.checkin_type layout="control" %}
|
{% bootstrap_field form.checkin_type layout="control" %}
|
||||||
|
{% bootstrap_field form.gate layout="control" %}
|
||||||
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
||||||
{% bootstrap_field form.questions_supported layout="control" %}
|
{% bootstrap_field form.questions_supported layout="control" %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% with id=widget.attrs.id %}<div{% if id %} id="{{ id }}"{% endif %} class="{{ widget.attrs.class }} checkbox">{% for group, options, index in widget.optgroups %}{% if group %}
|
||||||
|
<div class="checkbox-group"><div class="checkbox-group-legend"><span>{% if group.2 %}<i class="{{ group.2 }}" aria-hidden="true"></i> {% endif %}{{ group.0 }}</span></div><p class="text-muted">{{ group.1|safe }}</p>{% endif %}{% for option in options %}<div class="checkbox">
|
||||||
|
{% include option.template_name with widget=option %}</div>{% endfor %}{% if group %}
|
||||||
|
</div>{% endif %}{% endfor %}
|
||||||
|
</div>{% endwith %}
|
||||||
@@ -5,13 +5,12 @@
|
|||||||
{% include "django/forms/widgets/input.html" %}
|
{% include "django/forms/widgets/input.html" %}
|
||||||
{% if widget.wrap_label %}
|
{% if widget.wrap_label %}
|
||||||
{{ widget.label }}
|
{{ widget.label }}
|
||||||
{% if widget.incubating %}
|
{% if widget.incubating or not widget.official %}
|
||||||
<span class="label label-danger" data-toggle="tooltip" title="{% trans "The translation for this language is still in progress. This language can currently only be selected on development installations of pretix, not in production." %}">
|
|
||||||
{% trans "Translation in development" %}
|
<span class="label label-{% if widget.score > 50 %}success{% else %}{% if widget.score > 30 %}info{% else %}default{% endif %}{% endif %}"
|
||||||
</span>
|
data-toggle="tooltip"
|
||||||
{% elif not widget.official %}
|
title="{% trans "This percentage of texts is translated across all parts of the system including most plugins. Even a low value might be enough if you only use specific features. Untranslated texts will show up in English." %}">
|
||||||
<span class="label label-warning" data-toggle="tooltip" title="{% trans "This translation is not maintained by the pretix team. We cannot vouch for its correctness and new or recently changed features might not be translated and will show in English instead. You can help translating at translate.pretix.eu." %}">
|
{{ widget.score }} %
|
||||||
{% trans "Unofficial translation" %}
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends "pretixcontrol/event/base.html" %}
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans "Deny order" %}
|
{% trans "Deny order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -13,16 +14,7 @@
|
|||||||
|
|
||||||
<form method="post" href="">
|
<form method="post" href="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="checkbox">
|
{% bootstrap_form form %}
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="send_email" value="on" checked="checked">
|
|
||||||
{% trans "Notify user by e-mail" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<label>{% trans "Comment (will be sent to the user)" %}</label>
|
|
||||||
<textarea name="comment" class="form-control" rows="5"></textarea>
|
|
||||||
</p>
|
|
||||||
<div class="row checkout-button-row">
|
<div class="row checkout-button-row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<a class="btn btn-block btn-default btn-lg"
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
<input type="hidden" name="start-action" value="do_nothing">
|
<input type="hidden" name="start-action" value="do_nothing">
|
||||||
<input type="hidden" name="start-mode" value="partial">
|
<input type="hidden" name="start-mode" value="partial">
|
||||||
{% localize off %}
|
{% localize off %}
|
||||||
<input type="hidden" name="start-partial_amount" value="{{ overpaid|floatformat:2 }}">
|
<input type="hidden" name="start-partial_amount" value="{{ overpaid|money_numberfield:request.event.currency }}">
|
||||||
{% endlocalize %}
|
{% endlocalize %}
|
||||||
<input type="hidden" name="comment" value="{% trans "Refund for overpayment" %}">
|
<input type="hidden" name="comment" value="{% trans "Refund for overpayment" %}">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
|
|||||||
@@ -51,9 +51,9 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="refund-{{ p.pk }}"
|
<input type="text" name="refund-{{ p.pk }}"
|
||||||
{% if p.propose_refund %}
|
{% if p.propose_refund %}
|
||||||
value="{{ p.propose_refund|floatformat:2 }}"
|
value="{{ p.propose_refund|money_numberfield:request.event.currency }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
placeholder="{{ p.propose_refund|floatformat:2 }}"
|
placeholder="{{ p.propose_refund|money_numberfield:request.event.currency }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
title="" class="form-control">
|
title="" class="form-control">
|
||||||
<span class="input-group-addon">
|
<span class="input-group-addon">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
{% elif p.full_refund_possible %}
|
{% elif p.full_refund_possible %}
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" name="refund-{{ p.pk }}"
|
<input type="checkbox" name="refund-{{ p.pk }}"
|
||||||
value="{{ p.amount|floatformat:2 }}"
|
value="{{ p.amount|money_numberfield:request.event.currency }}"
|
||||||
{% if p.propose_refund == p.amount %}checked{% endif %}>
|
{% if p.propose_refund == p.amount %}checked{% endif %}>
|
||||||
{% trans "Full amount" %} ({{ p.amount|money:request.event.currency }})
|
{% trans "Full amount" %} ({{ p.amount|money:request.event.currency }})
|
||||||
</label>
|
</label>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<td class="text-right flip refund-amount">
|
<td class="text-right flip refund-amount">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="newrefund-{{ prov }}"
|
<input type="text" name="newrefund-{{ prov }}"
|
||||||
placeholder="{{ 0|floatformat:2 }}"
|
placeholder="{{ 0|money_numberfield:request.event.currency }}"
|
||||||
title="" class="form-control">
|
title="" class="form-control">
|
||||||
<span class="input-group-addon">
|
<span class="input-group-addon">
|
||||||
{{ request.event.currency }}
|
{{ request.event.currency }}
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<td class="text-right flip refund-amount">
|
<td class="text-right flip refund-amount">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="refund-offsetting"
|
<input type="text" name="refund-offsetting"
|
||||||
title="" class="form-control" placeholder="{{ 0|floatformat:2 }}">
|
title="" class="form-control" placeholder="{{ 0|money_numberfield:request.event.currency }}">
|
||||||
<span class="input-group-addon">
|
<span class="input-group-addon">
|
||||||
{{ request.event.currency }}
|
{{ request.event.currency }}
|
||||||
</span>
|
</span>
|
||||||
@@ -144,9 +144,9 @@
|
|||||||
<input type="text" name="refund-new-giftcard"
|
<input type="text" name="refund-new-giftcard"
|
||||||
title="" class="form-control"
|
title="" class="form-control"
|
||||||
{% if giftcard_proposal %}
|
{% if giftcard_proposal %}
|
||||||
value="{{ giftcard_proposal|floatformat:2 }}"
|
value="{{ giftcard_proposal|money_numberfield:request.event.currency }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
placeholder="{{ giftcard_proposal|floatformat:2 }}"
|
placeholder="{{ giftcard_proposal|money_numberfield:request.event.currency }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
<span class="input-group-addon">
|
<span class="input-group-addon">
|
||||||
@@ -172,9 +172,9 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="refund-manual"
|
<input type="text" name="refund-manual"
|
||||||
{% if remainder %}
|
{% if remainder %}
|
||||||
value="{{ remainder|floatformat:2 }}"
|
value="{{ remainder|money_numberfield:request.event.currency }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
placeholder="{{ remainder|floatformat:2 }}"
|
placeholder="{{ remainder|money_numberfield:request.event.currency }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
title="" class="form-control">
|
title="" class="form-control">
|
||||||
<span class="input-group-addon">
|
<span class="input-group-addon">
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load money %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Modify orders" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Modify orders" %}</h1>
|
||||||
|
<form action="{{ request.get_full_path }}" method="post" class="form-horizontal" data-asynctask
|
||||||
|
data-asynctask-long="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed with label=label allowed=allowed.count total=total %}
|
||||||
|
The operation <strong>{{ label }}</strong> can be applied to <strong>{{ allowed }}</strong> of the
|
||||||
|
selected <strong>{{ total }}</strong> orders.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% if allowed %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-condensed table-hover table-orders">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Order code" %}</th>
|
||||||
|
<th>{% trans "User" %}</th>
|
||||||
|
<th>{% trans "Order date" %}</th>
|
||||||
|
<th class="text-right flip">{% trans "Order total" %}</th>
|
||||||
|
<th class="text-right flip">{% trans "Status" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in allowed|slice:":50" %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||||
|
{{ o.code }}</a>
|
||||||
|
</strong>
|
||||||
|
{% if o.testmode %}
|
||||||
|
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ o.email|default_if_none:"" }}
|
||||||
|
{% if o.invoice_address.name %}
|
||||||
|
<br>{{ o.invoice_address.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||||
|
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||||
|
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">
|
||||||
|
{{ o.total|money:request.event.currency }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if allowed.count > 50 %}
|
||||||
|
<tr>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
<td>…</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Do you want to continue?
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This operation cannot be reversed.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
{% for k, l in request.POST.lists %}
|
||||||
|
{% if "bulkactionform" not in k %}
|
||||||
|
{% for v in l %}
|
||||||
|
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% bootstrap_form form layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-default btn-cancel">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
{% if allowed %}
|
||||||
|
<button type="submit" class="btn btn-primary btn-save" value="confirm" name="operation">
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -17,11 +17,12 @@
|
|||||||
|
|
||||||
{% if not request.event.live %}
|
{% if not request.event.live %}
|
||||||
<a href="{% url "control:event.live" event=request.event.slug organizer=request.event.organizer.slug %}"
|
<a href="{% url "control:event.live" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||||
class="btn btn-primary btn-lg">
|
class="btn btn-primary btn-lg">
|
||||||
{% trans "Take your shop live" %}
|
{% trans "Take your shop live" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg" target="_blank">
|
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg"
|
||||||
|
target="_blank">
|
||||||
{% trans "Go to the ticket shop" %}
|
{% trans "Go to the ticket shop" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -40,9 +41,10 @@
|
|||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form class="form-inline"
|
<form class="form-inline"
|
||||||
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
<p class="input-group">
|
<p class="input-group">
|
||||||
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus>
|
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}"
|
||||||
|
autofocus>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
|
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -82,7 +84,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right flip">
|
<div class="text-right flip">
|
||||||
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-default btn-lg">
|
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||||
|
class="btn btn-default btn-lg">
|
||||||
{% trans "Advanced search" %}
|
{% trans "Advanced search" %}
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-primary btn-lg" type="submit">
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
@@ -99,134 +102,224 @@
|
|||||||
{% blocktrans trimmed with question=filter_form.cleaned_data.question.question %}
|
{% blocktrans trimmed with question=filter_form.cleaned_data.question.question %}
|
||||||
List filtered by answers to question "{{ question }}".
|
List filtered by answers to question "{{ question }}".
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
<a href="?{% url_replace request 'question' '' 'answer' ''%}" class="text-muted">
|
<a href="?{% url_replace request 'question' '' 'answer' '' %}" class="text-muted">
|
||||||
<span class="fa fa-times"></span>
|
<span class="fa fa-times"></span>
|
||||||
{% trans "Remove filter" %}
|
{% trans "Remove filter" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="table-responsive">
|
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||||
<table class="table table-condensed table-hover table-orders">
|
{% csrf_token %}
|
||||||
<thead>
|
{% for field in filter_form %}
|
||||||
<tr>
|
{{ field.as_hidden }}
|
||||||
<th>{% trans "Order code" %}
|
{% endfor %}
|
||||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
{% for form in filter_forms %}
|
||||||
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a></th>
|
{% for field in form %}
|
||||||
<th>{% trans "User" %}
|
{{ field.as_hidden }}
|
||||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
|
|
||||||
<th>{% trans "Order date" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i class="fa fa-caret-up"></i></a>
|
|
||||||
</th>
|
|
||||||
<th class="text-right flip">{% trans "Order paid / total" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
|
|
||||||
<th class="text-right flip">{% trans "Positions" %}</th>
|
|
||||||
<th class="text-right flip">{% trans "Status" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-status' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for o in orders %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>
|
|
||||||
<a
|
|
||||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
|
||||||
{{ o.code }}</a>
|
|
||||||
</strong>
|
|
||||||
{% if o.testmode %}
|
|
||||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if o.custom_followup_due %}
|
|
||||||
<span class="label label-danger">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
|
||||||
{% elif o.custom_followup_at %}
|
|
||||||
<span class="label label-default">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ o.email|default_if_none:"" }}
|
|
||||||
{% if o.invoice_address.name %}
|
|
||||||
<br>{{ o.invoice_address.name }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
|
||||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
|
||||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
|
||||||
</td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
{% if o.has_cancellation_request %}
|
|
||||||
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if o.has_external_refund or o.has_pending_refund %}
|
|
||||||
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
|
||||||
{% elif o.has_pending_refund %}
|
|
||||||
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if o.is_overpaid %}
|
|
||||||
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
|
||||||
{% elif o.is_underpaid %}
|
|
||||||
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
|
||||||
{% elif o.is_pending_with_full_payment %}
|
|
||||||
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
|
||||||
<span class="text-muted">
|
|
||||||
{% endif %}
|
|
||||||
{{ o.computed_payment_refund_sum|money:request.event.currency }} /
|
|
||||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{{ o.total|money:request.event.currency }}
|
|
||||||
{% if o.status == "c" and o.icnt %}
|
|
||||||
<br>
|
|
||||||
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
|
||||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
{% endfor %}
|
||||||
{% if sums %}
|
<div class="table-responsive">
|
||||||
<tfoot>
|
<table class="table table-condensed table-hover table-orders">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Sum over all pages" %}</th>
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
<th></th>
|
<th>
|
||||||
<th>
|
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||||
{% blocktrans trimmed count s=sums.c %}
|
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||||
1 order
|
</th>
|
||||||
{% plural %}
|
{% endif %}
|
||||||
{{ s }} orders
|
<th>{% trans "Order code" %}
|
||||||
{% endblocktrans %}
|
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right flip">
|
<th>{% trans "User" %}
|
||||||
{% if not filter_form.filtered %}
|
<a href="?{% url_replace request 'ordering' '-email' %}"><i
|
||||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
class="fa fa-caret-down"></i></a>
|
||||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
{% if sums.s|default_if_none:"none" != "none" %}
|
|
||||||
{{ sums.s|money:request.event.currency }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right flip">
|
<th>{% trans "Order date" %}
|
||||||
{% if not filter_form.filtered %}
|
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i
|
||||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
class="fa fa-caret-up"></i></a>
|
||||||
{% if sums.pc %}
|
</th>
|
||||||
{{ sums.pc }}
|
<th class="text-right flip">{% trans "Order paid / total" %}
|
||||||
{% endif %}
|
<a href="?{% url_replace request 'ordering' '-total' %}"><i
|
||||||
{% endif %}
|
class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
|
</th>
|
||||||
|
<th class="text-right flip">{% trans "Positions" %}</th>
|
||||||
|
<th class="text-right flip">{% trans "Status" %}
|
||||||
|
<a href="?{% url_replace request 'ordering' '-status' %}"><i
|
||||||
|
class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
{% if page_obj.paginator.num_pages > 1 and "can_change_orders" in request.eventpermset %}
|
||||||
{% endif %}
|
<tr class="table-select-all warning hidden">
|
||||||
</table>
|
<td>
|
||||||
</div>
|
<input type="checkbox" name="__ALL" id="__all"
|
||||||
|
data-results-total="{{ page_obj.paginator.count }}">
|
||||||
|
</td>
|
||||||
|
<td colspan="6">
|
||||||
|
<label for="__all">
|
||||||
|
{% trans "Select all results on other pages as well" %}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in orders %}
|
||||||
|
<tr>
|
||||||
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
|
<td>
|
||||||
|
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||||
|
class="batch-select-label"><input type="checkbox" name="order"
|
||||||
|
class="batch-select-checkbox"
|
||||||
|
value="{{ o.pk }}"/></label>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
<a
|
||||||
|
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||||
|
{{ o.code }}</a>
|
||||||
|
</strong>
|
||||||
|
{% if o.testmode %}
|
||||||
|
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if o.custom_followup_due %}
|
||||||
|
<span class="label label-danger">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||||
|
{% elif o.custom_followup_at %}
|
||||||
|
<span class="label label-default">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ o.email|default_if_none:"" }}
|
||||||
|
{% if o.invoice_address.name %}
|
||||||
|
<br>{{ o.invoice_address.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||||
|
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||||
|
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">
|
||||||
|
{% if o.has_cancellation_request %}
|
||||||
|
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if o.has_external_refund or o.has_pending_refund %}
|
||||||
|
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
||||||
|
{% elif o.has_pending_refund %}
|
||||||
|
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if o.is_overpaid %}
|
||||||
|
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
||||||
|
{% elif o.is_underpaid %}
|
||||||
|
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
||||||
|
{% elif o.is_pending_with_full_payment %}
|
||||||
|
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||||
|
<span class="text-muted">
|
||||||
|
{% endif %}
|
||||||
|
{{ o.computed_payment_refund_sum|money:request.event.currency }} /
|
||||||
|
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{{ o.total|money:request.event.currency }}
|
||||||
|
{% if o.status == "c" and o.icnt %}
|
||||||
|
<br>
|
||||||
|
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
||||||
|
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% if sums %}
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{% trans "Sum over all pages" %}</th>
|
||||||
|
<th></th>
|
||||||
|
<th>
|
||||||
|
{% blocktrans trimmed count s=sums.c %}
|
||||||
|
1 order
|
||||||
|
{% plural %}
|
||||||
|
{{ s }} orders
|
||||||
|
{% endblocktrans %}
|
||||||
|
</th>
|
||||||
|
<th class="text-right flip">
|
||||||
|
{% if not filter_form.filtered %}
|
||||||
|
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||||
|
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||||
|
{% if sums.s|default_if_none:"none" != "none" %}
|
||||||
|
{{ sums.s|money:request.event.currency }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th class="text-right flip">
|
||||||
|
{% if not filter_form.filtered %}
|
||||||
|
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||||
|
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||||
|
{% if sums.pc %}
|
||||||
|
{{ sums.pc }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
|
<div class="batch-select-actions">
|
||||||
|
<div class="btn-group dropup">
|
||||||
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
||||||
|
aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="caret"></span>
|
||||||
|
{% trans "Select action" %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
formaction="{% url "control:event.orders.bulk.approve" organizer=request.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-thumbs-up fa-fw text-green"></i>
|
||||||
|
{% trans "Approve" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
formaction="{% url "control:event.orders.bulk.deny" organizer=request.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-thumbs-down fa-fw text-danger"></i>
|
||||||
|
{% trans "Deny" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% if not request.event.settings.payment_term_expire_automatically %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
formaction="{% url "control:event.orders.bulk.expire" organizer=request.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-times fa-fw"></i>
|
||||||
|
{% trans "Mark as expired if overdue" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
formaction="{% url "control:event.orders.bulk.delete" organizer=request.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-trash fa-fw text-danger"></i>
|
||||||
|
{% trans "Delete (test mode only)" %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
You can invite other organizers to accept your gift cards. After you have done so, they need to go
|
You can invite other organizers to accept your gift cards. After you have done so, they need to go
|
||||||
to the same page in their account and accept your invitation. Note that other organizers will be able
|
to the same page in their account and accept your invitation. Note that other organizers will be able
|
||||||
to add money to gift cards as well that you will need to collect form them. It is your responsibility
|
to add money to gift cards as well that you will need to collect from them. It is your responsibility
|
||||||
to handle the exchange of money to offset the transactions between the two organizers.
|
to handle the exchange of money to offset the transactions between the two organizers.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -77,9 +77,9 @@
|
|||||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if o.custom_followup_due %}
|
{% if o.custom_followup_due %}
|
||||||
<span class="label label-danger">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
<span class="label label-danger">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||||
{% elif o.custom_followup_at %}
|
{% elif o.custom_followup_at %}
|
||||||
<span class="label label-default">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
<span class="label label-default">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ o.event.name }}</td>
|
<td>{{ o.event.name }}</td>
|
||||||
|
|||||||
@@ -114,10 +114,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group submit-group">
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<div class="form-group submit-group">
|
||||||
{% trans "Save" %}
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
</button>
|
{% trans "Save" %}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -72,18 +72,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create multiple new vouchers" %}</a>
|
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create multiple new vouchers" %}</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
|
||||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
{% trans "Create multiple new vouchers" %}</a>
|
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||||
|
{% trans "Create multiple new vouchers" %}</a>
|
||||||
|
{% endif %}
|
||||||
<a href="?{% url_replace request "download" "yes" %}"
|
<a href="?{% url_replace request "download" "yes" %}"
|
||||||
class="btn btn-default"><i class="fa fa-download"></i>
|
class="btn btn-default"><i class="fa fa-download"></i>
|
||||||
{% trans "Download list" %}</a>
|
{% trans "Download list" %}</a>
|
||||||
@@ -94,13 +98,13 @@
|
|||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
{% if "can_change_vouchers" in request.eventpermset %}
|
<th>
|
||||||
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
|
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
|
||||||
<input type="checkbox" data-toggle-table />
|
<input type="checkbox" data-toggle-table />
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
</th>
|
||||||
</th>
|
{% endif %}
|
||||||
<th>
|
<th>
|
||||||
{% trans "Voucher code" %}
|
{% trans "Voucher code" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
@@ -139,13 +143,13 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for v in vouchers %}
|
{% for v in vouchers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
{% if "can_change_vouchers" in request.eventpermset %}
|
<td>
|
||||||
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
|
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
|
||||||
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
|
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
</td>
|
||||||
</td>
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% if not v.is_active %}<del>{% endif %}
|
{% if not v.is_active %}<del>{% endif %}
|
||||||
<strong><a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
|
<strong><a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
|
||||||
@@ -186,11 +190,13 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ v.id }}"
|
{% if "can_change_vouchers" in request.eventpermset %}
|
||||||
class="btn btn-sm btn-default" title="{% trans "Use as a template for new vouchers" %}" data-toggle="tooltip">
|
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ v.id }}"
|
||||||
<span class="fa fa-copy"></span>
|
class="btn btn-sm btn-default" title="{% trans "Use as a template for new vouchers" %}" data-toggle="tooltip">
|
||||||
</a>
|
<span class="fa fa-copy"></span>
|
||||||
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
</a>
|
||||||
|
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -414,6 +414,10 @@ urlpatterns = [
|
|||||||
re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||||
re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||||
re_path(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
re_path(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||||
|
re_path(r'^orders/bulk/approve$', orders.OrderApproveBulkActionView.as_view(), name='event.orders.bulk.approve'),
|
||||||
|
re_path(r'^orders/bulk/deny$', orders.OrderDenyBulkActionView.as_view(), name='event.orders.bulk.deny'),
|
||||||
|
re_path(r'^orders/bulk/expire$', orders.OrderExpireBulkActionView.as_view(), name='event.orders.bulk.expire'),
|
||||||
|
re_path(r'^orders/bulk/delete$', orders.OrderDeleteBulkActionView.as_view(), name='event.orders.bulk.delete'),
|
||||||
re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
||||||
re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
||||||
re_path(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
re_path(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ from pretix.control.forms.checkin import (
|
|||||||
CheckinListForm, CheckinListSimulatorForm,
|
CheckinListForm, CheckinListSimulatorForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
CheckinFilterForm, CheckinListAttendeeFilterForm,
|
CheckinFilterForm, CheckinListAttendeeFilterForm, CheckinListFilterForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
||||||
@@ -192,7 +192,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
|||||||
|
|
||||||
|
|
||||||
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
|
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
|
||||||
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
|
||||||
permission = ('can_change_orders', 'can_checkin_orders')
|
permission = ('can_change_orders', 'can_checkin_orders')
|
||||||
context_object_name = 'device'
|
context_object_name = 'device'
|
||||||
|
|
||||||
@@ -279,13 +278,13 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
context_object_name = 'checkinlists'
|
context_object_name = 'checkinlists'
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
template_name = 'pretixcontrol/checkin/lists.html'
|
template_name = 'pretixcontrol/checkin/lists.html'
|
||||||
|
ordering = ('subevent__date_from', 'name', 'pk')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
|
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
|
||||||
|
|
||||||
if self.request.GET.get("subevent", "") != "":
|
if self.filter_form.is_valid():
|
||||||
s = self.request.GET.get("subevent", "")
|
qs = self.filter_form.filter_qs(qs)
|
||||||
qs = qs.filter(subevent_id=s)
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@@ -304,9 +303,14 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
'can_change_organizer_settings',
|
'can_change_organizer_settings',
|
||||||
self.request
|
self.request
|
||||||
)
|
)
|
||||||
|
ctx['filter_form'] = self.filter_form
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return CheckinListFilterForm(data=self.request.GET, event=self.request.event)
|
||||||
|
|
||||||
|
|
||||||
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
|
class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
@@ -491,6 +495,11 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
|||||||
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['event'] = self.request.event
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
return {
|
return {
|
||||||
'datetime': now()
|
'datetime': now()
|
||||||
@@ -524,12 +533,13 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
|||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
legacy_url_support=False,
|
legacy_url_support=False,
|
||||||
simulate=True,
|
simulate=True,
|
||||||
|
gate=form.cleaned_data.get("gate"),
|
||||||
).data
|
).data
|
||||||
|
|
||||||
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
||||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
|
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"], form.cleaned_data.get("gate"))
|
||||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
||||||
form.cleaned_data["datetime"])
|
form.cleaned_data["datetime"])
|
||||||
self.result["rule_graph"] = rule_graph
|
self.result["rule_graph"] = rule_graph
|
||||||
|
|||||||
@@ -1527,7 +1527,7 @@ class QuickSetupView(FormView):
|
|||||||
|
|
||||||
|
|
||||||
class EventQRCode(EventPermissionRequiredMixin, View):
|
class EventQRCode(EventPermissionRequiredMixin, View):
|
||||||
permission = 'can_change_event_settings'
|
permission = None
|
||||||
|
|
||||||
def get(self, request, *args, filetype, **kwargs):
|
def get(self, request, *args, filetype, **kwargs):
|
||||||
url = build_absolute_uri(request.event, 'presale:event.index')
|
url = build_absolute_uri(request.event, 'presale:event.index')
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ from urllib.parse import quote, urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
||||||
Subquery, Sum,
|
QuerySet, Subquery, Sum,
|
||||||
)
|
)
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
from django.http import (
|
from django.http import (
|
||||||
@@ -111,16 +111,16 @@ from pretix.base.signals import (
|
|||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||||
from pretix.base.views.tasks import AsyncAction
|
from pretix.base.views.tasks import AsyncAction, AsyncFormView
|
||||||
from pretix.control.forms.exports import ScheduledEventExportForm
|
from pretix.control.forms.exports import ScheduledEventExportForm
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||||
RefundFilterForm,
|
RefundFilterForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.orders import (
|
from pretix.control.forms.orders import (
|
||||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
CancelForm, CommentForm, ConfirmPaymentForm, DenyForm, EventCancelForm,
|
||||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
|
||||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||||
)
|
)
|
||||||
@@ -137,10 +137,17 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class OrderSearchMixin:
|
class OrderSearchMixin:
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def request_data(self):
|
||||||
|
if self.request.method == "POST":
|
||||||
|
return self.request.POST
|
||||||
|
return self.request.GET
|
||||||
|
|
||||||
def get_forms(self):
|
def get_forms(self):
|
||||||
f = [
|
f = [
|
||||||
EventOrderExpertFilterForm(
|
EventOrderExpertFilterForm(
|
||||||
data=self.request.GET,
|
data=self.request_data,
|
||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
prefix='expert',
|
prefix='expert',
|
||||||
)
|
)
|
||||||
@@ -160,6 +167,167 @@ class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, AsyncFormView):
|
||||||
|
template_name = 'pretixcontrol/orders/bulk_action.html'
|
||||||
|
permission = 'can_change_orders'
|
||||||
|
form_class = forms.Form
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = Order.objects.filter(
|
||||||
|
event=self.request.event
|
||||||
|
).select_related('invoice_address')
|
||||||
|
|
||||||
|
if self.filter_form.is_valid():
|
||||||
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
|
for f in self.get_forms():
|
||||||
|
if any(k.startswith(f.prefix) for k in self.request.POST.keys()):
|
||||||
|
if not f.is_valid():
|
||||||
|
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||||
|
qs = f.filter_qs(qs)
|
||||||
|
|
||||||
|
if 'order' in self.request_data and '__ALL' not in self.request_data:
|
||||||
|
qs = qs.filter(
|
||||||
|
id__in=self.request_data.getlist('order')
|
||||||
|
)
|
||||||
|
elif '__ALL' not in self.request_data:
|
||||||
|
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return EventOrderFilterForm(data=self.request.POST, event=self.request.event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def allowed_for(self, queryset: QuerySet) -> QuerySet:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def execute_single(self, instance, form: forms.Form):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
||||||
|
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
|
||||||
|
total = qs.count()
|
||||||
|
for i, o in enumerate(qs):
|
||||||
|
self.execute_single(o, form)
|
||||||
|
if i % 100 == 0:
|
||||||
|
self.async_set_progress(i / total * 100)
|
||||||
|
|
||||||
|
def get_error_url(self):
|
||||||
|
return self.get_success_url(None)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||||
|
return self.get_result(request)
|
||||||
|
return render(request, self.template_name, self.get_context_data())
|
||||||
|
|
||||||
|
def get_success_url(self, value):
|
||||||
|
return reverse('control:event.orders', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['total'] = self.get_queryset().count()
|
||||||
|
ctx['allowed'] = self.allowed_for(self.get_queryset())
|
||||||
|
ctx['label'] = self.label
|
||||||
|
ctx['form'] = self.get_form()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = {
|
||||||
|
"initial": self.get_initial(),
|
||||||
|
"prefix": self.get_prefix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.request.method in ("POST", "PUT") and self.request.POST.get("operation") == "confirm":
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"data": self.request.POST,
|
||||||
|
"files": self.request.FILES,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Handle POST requests: instantiate a form instance with the passed
|
||||||
|
POST variables and then check if it's valid.
|
||||||
|
"""
|
||||||
|
form = self.get_form()
|
||||||
|
if self.request.POST.get("operation") == "confirm" and form.is_valid():
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def get_prefix(self):
|
||||||
|
return "bulkactionform"
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def async_form_valid(self, task, form):
|
||||||
|
self.execute_bulk(self.allowed_for(self.get_queryset()), form)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderApproveBulkActionView(BaseOrderBulkActionView):
|
||||||
|
label = _("Approve")
|
||||||
|
|
||||||
|
def allowed_for(self, queryset):
|
||||||
|
return queryset.filter(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_single(self, instance, form: forms.Form):
|
||||||
|
approve_order(instance, user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderDenyBulkActionView(BaseOrderBulkActionView):
|
||||||
|
label = _("Deny")
|
||||||
|
form_class = DenyForm
|
||||||
|
|
||||||
|
def allowed_for(self, queryset):
|
||||||
|
return queryset.filter(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_single(self, instance, form: forms.Form):
|
||||||
|
deny_order(instance, user=self.request.user,
|
||||||
|
comment=form.cleaned_data.get('comment') or None,
|
||||||
|
send_mail=form.cleaned_data['send_email'])
|
||||||
|
|
||||||
|
|
||||||
|
class OrderExpireBulkActionView(BaseOrderBulkActionView):
|
||||||
|
label = _("Mark as expired if overdue")
|
||||||
|
|
||||||
|
def allowed_for(self, queryset):
|
||||||
|
return queryset.filter(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=False,
|
||||||
|
expires__lt=now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_single(self, instance, form: forms.Form):
|
||||||
|
mark_order_expired(instance, user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
||||||
|
label = _("Delete")
|
||||||
|
|
||||||
|
def allowed_for(self, queryset):
|
||||||
|
return queryset.filter(
|
||||||
|
testmode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_single(self, instance, form: forms.Form):
|
||||||
|
instance.gracefully_delete(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
model = Order
|
model = Order
|
||||||
context_object_name = 'orders'
|
context_object_name = 'orders'
|
||||||
@@ -183,6 +351,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
|
ctx['filter_forms'] = self.get_forms()
|
||||||
|
|
||||||
ctx['filter_strings'] = []
|
ctx['filter_strings'] = []
|
||||||
for f in self.get_forms():
|
for f in self.get_forms():
|
||||||
@@ -607,21 +776,26 @@ class OrderDelete(OrderView):
|
|||||||
class OrderDeny(OrderView):
|
class OrderDeny(OrderView):
|
||||||
permission = 'can_change_orders'
|
permission = 'can_change_orders'
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if self.order.require_approval:
|
if self.order.require_approval:
|
||||||
try:
|
form = DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||||
deny_order(self.order, user=self.request.user,
|
if form.is_valid():
|
||||||
comment=self.request.POST.get('comment'),
|
try:
|
||||||
send_mail=self.request.POST.get('send_email') == 'on')
|
deny_order(self.order, user=self.request.user,
|
||||||
except OrderError as e:
|
comment=self.request.POST.get('comment'),
|
||||||
messages.error(self.request, str(e))
|
send_mail=self.request.POST.get('send_email') == 'on')
|
||||||
|
except OrderError as e:
|
||||||
|
messages.error(self.request, str(e))
|
||||||
|
else:
|
||||||
|
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||||
else:
|
else:
|
||||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
return self.get(request, *args, **kwargs)
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
return render(self.request, 'pretixcontrol/order/deny.html', {
|
return render(self.request, 'pretixcontrol/order/deny.html', {
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
|
'form': DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1298,7 +1472,7 @@ class OrderTransition(OrderView):
|
|||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'code': self.order.code
|
'code': self.order.code
|
||||||
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}&comment={}'.format(
|
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}&comment={}'.format(
|
||||||
round_decimal(self.order.pending_sum * -1),
|
round_decimal(self.order.pending_sum * -1, self.order.event.currency),
|
||||||
'true' if self.req and self.req.refund_as_giftcard else 'false',
|
'true' if self.req and self.req.refund_as_giftcard else 'false',
|
||||||
quote(gettext('Order canceled'))
|
quote(gettext('Order canceled'))
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ def devices_select2(request, **kwargs):
|
|||||||
return JsonResponse(doc)
|
return JsonResponse(doc)
|
||||||
|
|
||||||
|
|
||||||
@organizer_permission_required(("can_view_orders", "can_change_organizer_settings"))
|
@organizer_permission_required(("can_view_orders", "can_change_event_settings", "can_change_organizer_settings"))
|
||||||
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
||||||
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
||||||
def gate_select2(request, **kwargs):
|
def gate_select2(request, **kwargs):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import bleach
|
|||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Exists, OuterRef, Sum
|
from django.db.models import Exists, OuterRef, Sum
|
||||||
from django.http import (
|
from django.http import (
|
||||||
@@ -64,7 +64,6 @@ from pretix.base.models import (
|
|||||||
CartPosition, LogEntry, Voucher, WaitingListEntry,
|
CartPosition, LogEntry, Voucher, WaitingListEntry,
|
||||||
)
|
)
|
||||||
from pretix.base.models.vouchers import generate_codes
|
from pretix.base.models.vouchers import generate_codes
|
||||||
from pretix.base.services.locking import NoLockManager
|
|
||||||
from pretix.base.services.vouchers import vouchers_send
|
from pretix.base.services.vouchers import vouchers_send
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.base.views.tasks import AsyncFormView
|
from pretix.base.views.tasks import AsyncFormView
|
||||||
@@ -270,7 +269,7 @@ class VoucherDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
|||||||
class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||||
model = Voucher
|
model = Voucher
|
||||||
template_name = 'pretixcontrol/vouchers/detail.html'
|
template_name = 'pretixcontrol/vouchers/detail.html'
|
||||||
permission = 'can_change_vouchers'
|
permission = ('can_change_vouchers', 'can_view_vouchers')
|
||||||
context_object_name = 'voucher'
|
context_object_name = 'voucher'
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
@@ -284,6 +283,14 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
form_class = response
|
form_class = response
|
||||||
return form_class
|
return form_class
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_change_vouchers',
|
||||||
|
request=self.request):
|
||||||
|
for f in form.fields.values():
|
||||||
|
f.disabled = True
|
||||||
|
return form
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> VoucherForm:
|
def get_object(self, queryset=None) -> VoucherForm:
|
||||||
url = resolve(self.request.path_info)
|
url = resolve(self.request.path_info)
|
||||||
try:
|
try:
|
||||||
@@ -293,7 +300,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
except Voucher.DoesNotExist:
|
except Voucher.DoesNotExist:
|
||||||
raise Http404(_("The requested voucher does not exist."))
|
raise Http404(_("The requested voucher does not exist."))
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
if form.has_changed():
|
if form.has_changed():
|
||||||
@@ -304,6 +310,13 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
)
|
)
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers',
|
||||||
|
request=request):
|
||||||
|
raise PermissionDenied()
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
return reverse('control:event.vouchers', kwargs={
|
return reverse('control:event.vouchers', kwargs={
|
||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
@@ -355,7 +368,6 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
kwargs['instance'] = Voucher(event=self.request.event)
|
kwargs['instance'] = Voucher(event=self.request.event)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.event = self.request.event
|
form.instance.event = self.request.event
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
@@ -370,10 +382,9 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user)
|
form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
# TODO: Transform this into an asynchronous call?
|
return super().post(request, *args, **kwargs)
|
||||||
with request.event.lock():
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherGo(EventPermissionRequiredMixin, View):
|
class VoucherGo(EventPermissionRequiredMixin, View):
|
||||||
@@ -398,6 +409,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
|||||||
template_name = 'pretixcontrol/vouchers/bulk.html'
|
template_name = 'pretixcontrol/vouchers/bulk.html'
|
||||||
permission = 'can_change_vouchers'
|
permission = 'can_change_vouchers'
|
||||||
context_object_name = 'voucher'
|
context_object_name = 'voucher'
|
||||||
|
atomic_execute = True
|
||||||
|
|
||||||
def get_success_url(self, value) -> str:
|
def get_success_url(self, value) -> str:
|
||||||
return reverse('control:event.vouchers', kwargs={
|
return reverse('control:event.vouchers', kwargs={
|
||||||
@@ -437,9 +449,6 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
|||||||
return form_kwargs
|
return form_kwargs
|
||||||
|
|
||||||
def async_form_valid(self, task, form):
|
def async_form_valid(self, task, form):
|
||||||
lockfn = NoLockManager
|
|
||||||
if form.data.get('block_quota'):
|
|
||||||
lockfn = self.request.event.lock
|
|
||||||
batch_size = 500
|
batch_size = 500
|
||||||
total_num = 1 # will be set later
|
total_num = 1 # will be set later
|
||||||
|
|
||||||
@@ -473,27 +482,26 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
|||||||
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
|
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
|
||||||
|
|
||||||
voucherids = []
|
voucherids = []
|
||||||
with lockfn(), transaction.atomic():
|
if not form.is_valid():
|
||||||
if not form.is_valid():
|
raise ValidationError(form.errors)
|
||||||
raise ValidationError(form.errors)
|
total_num = len(form.cleaned_data['codes'])
|
||||||
total_num = len(form.cleaned_data['codes'])
|
|
||||||
|
|
||||||
batch_vouchers = []
|
batch_vouchers = []
|
||||||
for code in form.cleaned_data['codes']:
|
for code in form.cleaned_data['codes']:
|
||||||
if len(batch_vouchers) >= batch_size:
|
if len(batch_vouchers) >= batch_size:
|
||||||
process_batch(batch_vouchers, voucherids)
|
process_batch(batch_vouchers, voucherids)
|
||||||
|
|
||||||
obj = modelcopy(form.instance, code=None)
|
obj = modelcopy(form.instance, code=None)
|
||||||
obj.event = self.request.event
|
obj.event = self.request.event
|
||||||
obj.code = code
|
obj.code = code
|
||||||
try:
|
try:
|
||||||
obj.seat = form.cleaned_data['seats'].pop()
|
obj.seat = form.cleaned_data['seats'].pop()
|
||||||
obj.item = obj.seat.product
|
obj.item = obj.seat.product
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
batch_vouchers.append(obj)
|
batch_vouchers.append(obj)
|
||||||
|
|
||||||
process_batch(batch_vouchers, voucherids)
|
process_batch(batch_vouchers, voucherids)
|
||||||
|
|
||||||
if form.cleaned_data['send']:
|
if form.cleaned_data['send']:
|
||||||
vouchers_send(
|
vouchers_send(
|
||||||
@@ -525,6 +533,10 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
|||||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
|
class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
|
||||||
permission = 'can_change_vouchers'
|
permission = 'can_change_vouchers'
|
||||||
|
|||||||
@@ -19,12 +19,18 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import gettext as gettext_module
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
|
from django.utils.translation import to_locale
|
||||||
|
from django.utils.translation.trans_real import TranslationCatalog
|
||||||
|
|
||||||
date_conversion_to_moment = {
|
date_conversion_to_moment = {
|
||||||
'%a': 'ddd',
|
'%a': 'ddd',
|
||||||
@@ -156,3 +162,63 @@ def get_moment_locale(locale=None):
|
|||||||
|
|
||||||
def i18ncomp(query):
|
def i18ncomp(query):
|
||||||
return json.dumps(str(query))[1:-1]
|
return json.dumps(str(query))[1:-1]
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_language_score(locale):
|
||||||
|
"""
|
||||||
|
For a given language code, return a numeric score on how well-translated the language is. The score
|
||||||
|
is an integer greater than 1 and can be arbitrarily high, so it's only useful for comparing with
|
||||||
|
other languages.
|
||||||
|
|
||||||
|
Note that there is no valid score for "en", since it's technically not "translated".
|
||||||
|
"""
|
||||||
|
catalog = None
|
||||||
|
app_configs = reversed(apps.get_app_configs())
|
||||||
|
|
||||||
|
for app in app_configs:
|
||||||
|
# Filter out all third-party apps by looking for the pretix name and for valid pretix plugins
|
||||||
|
if not app.name.startswith("pretix") and not hasattr(app, 'PretixPluginMeta'):
|
||||||
|
continue
|
||||||
|
if hasattr(app, 'PretixPluginMeta'):
|
||||||
|
# Filter out invisible plugins and plugins only available to some users
|
||||||
|
p = app.PretixPluginMeta
|
||||||
|
if not getattr(p, 'visible', True) or hasattr(app, 'is_available'):
|
||||||
|
continue
|
||||||
|
localedir = os.path.join(app.path, "locale")
|
||||||
|
if os.path.exists(localedir):
|
||||||
|
try:
|
||||||
|
translation = gettext_module.translation(
|
||||||
|
domain="django",
|
||||||
|
localedir=localedir,
|
||||||
|
languages=[to_locale(locale)],
|
||||||
|
fallback=False,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if catalog is None:
|
||||||
|
catalog = TranslationCatalog(translation)
|
||||||
|
else:
|
||||||
|
catalog.update(translation)
|
||||||
|
|
||||||
|
# Add pretix' main translation folder as well as installation-specific translation folders
|
||||||
|
for localedir in reversed(settings.LOCALE_PATHS):
|
||||||
|
try:
|
||||||
|
translation = gettext_module.translation(
|
||||||
|
domain="django",
|
||||||
|
localedir=localedir,
|
||||||
|
languages=[to_locale(locale)],
|
||||||
|
fallback=False,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if catalog is None:
|
||||||
|
catalog = TranslationCatalog(translation)
|
||||||
|
else:
|
||||||
|
catalog.update(translation)
|
||||||
|
|
||||||
|
if not catalog:
|
||||||
|
score = 1
|
||||||
|
else:
|
||||||
|
score = len(list(catalog.items())) or 1
|
||||||
|
return score
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
|
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
|
||||||
@@ -51,7 +52,7 @@ def validate_uploaded_file_for_valid_image(f):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
image = Image.open(file)
|
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||||
# verify() must be called immediately after the constructor.
|
# verify() must be called immediately after the constructor.
|
||||||
image.verify()
|
image.verify()
|
||||||
except DecompressionBombError:
|
except DecompressionBombError:
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
#
|
#
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_vobject_performance():
|
def monkeypatch_vobject_performance():
|
||||||
"""
|
"""
|
||||||
@@ -52,5 +54,19 @@ def monkeypatch_vobject_performance():
|
|||||||
icalendar.tzinfo_eq = new_tzinfo_eq
|
icalendar.tzinfo_eq = new_tzinfo_eq
|
||||||
|
|
||||||
|
|
||||||
|
def monkeypatch_pillow_safer():
|
||||||
|
"""
|
||||||
|
Pillow supports many file formats, among them EPS. For EPS, Pillow loads GhostScript whenever GhostScript
|
||||||
|
is installed (cannot officially be disabled). However, GhostScript is known for regular security vulnerabilities.
|
||||||
|
We have no use of reading EPS files and usually prevent this by using `Image.open(…, formats=[…])` to disable EPS
|
||||||
|
support explicitly. However, we are worried about our dependencies like reportlab using `Image.open` without the
|
||||||
|
`formats=` parameter. Therefore, as a defense in depth approach, we monkeypatch EPS support away by modifying the
|
||||||
|
internal image format registry of Pillow.
|
||||||
|
"""
|
||||||
|
if "EPS" in Image.ID:
|
||||||
|
Image.ID.remove("EPS")
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_all_at_ready():
|
def monkeypatch_all_at_ready():
|
||||||
monkeypatch_vobject_performance()
|
monkeypatch_vobject_performance()
|
||||||
|
monkeypatch_pillow_safer()
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from arabic_reshaper import ArabicReshaper
|
from arabic_reshaper import ArabicReshaper
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from PIL.Image import Resampling
|
from PIL import Image
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
height = width * self._image.size[1] / self._image.size[0]
|
height = width * self._image.size[1] / self._image.size[0]
|
||||||
self._image.thumbnail(
|
self._image.thumbnail(
|
||||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||||
resample=Resampling.BICUBIC
|
resample=Image.Resampling.BICUBIC
|
||||||
)
|
)
|
||||||
self._data = None
|
self._data = None
|
||||||
return width, height
|
return width, height
|
||||||
@@ -44,6 +45,9 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
# (smaller) size of the modified image.
|
# (smaller) size of the modified image.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _read_image(self, fp):
|
||||||
|
return Image.open(fp, formats=settings.PILLOW_FORMATS_IMAGE)
|
||||||
|
|
||||||
|
|
||||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||||
'delete_harakat': True,
|
'delete_harakat': True,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import hashlib
|
|||||||
import math
|
import math
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from PIL import Image, ImageOps, ImageSequence
|
from PIL import Image, ImageOps, ImageSequence
|
||||||
@@ -165,7 +166,7 @@ def resize_image(image, size):
|
|||||||
|
|
||||||
def create_thumbnail(sourcename, size):
|
def create_thumbnail(sourcename, size):
|
||||||
source = default_storage.open(sourcename)
|
source = default_storage.open(sourcename)
|
||||||
image = Image.open(BytesIO(source.read()))
|
image = Image.open(BytesIO(source.read()), formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||||
try:
|
try:
|
||||||
image.load()
|
image.load()
|
||||||
except:
|
except:
|
||||||
|
|||||||
+1800
-1600
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,968 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-09-06 08:12+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
||||||
|
msgid "Marked as paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||||
|
msgid "Comment:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||||
|
msgid "PayPal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
||||||
|
msgid "Venmo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:38
|
||||||
|
msgid "Apple Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
|
||||||
|
msgid "Itaú"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
|
||||||
|
msgid "PayPal Credit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
|
||||||
|
msgid "Credit Card"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
|
||||||
|
msgid "PayPal Pay Later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
|
||||||
|
msgid "iDEAL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||||
|
msgid "SEPA Direct Debit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
||||||
|
msgid "Bancontact"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
|
||||||
|
msgid "giropay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
|
||||||
|
msgid "SOFORT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
|
||||||
|
msgid "eps"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
|
||||||
|
msgid "MyBank"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
|
||||||
|
msgid "Przelewy24"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
|
||||||
|
msgid "Verkkopankki"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50
|
||||||
|
msgid "PayU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
|
||||||
|
msgid "BLIK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
|
||||||
|
msgid "Trustly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53
|
||||||
|
msgid "Zimpler"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54
|
||||||
|
msgid "Maxima"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55
|
||||||
|
msgid "OXXO"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56
|
||||||
|
msgid "Boleto"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
|
||||||
|
msgid "WeChat Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
|
||||||
|
msgid "Mercado Pago"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:164
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
|
||||||
|
msgid "Confirming your payment …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:247
|
||||||
|
msgid "Payment method unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Placed orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Paid orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||||
|
msgid "Total revenue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
|
||||||
|
msgid "Contacting Stripe …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
|
||||||
|
msgid "Total"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
|
||||||
|
msgid "Contacting your bank …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
|
||||||
|
msgid "Select a check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
|
||||||
|
msgid "No active check-in lists found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
|
||||||
|
msgid "Switch check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
|
||||||
|
msgid "Search results"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
|
||||||
|
msgid "No tickets found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
|
||||||
|
msgid "Result"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
|
||||||
|
msgid "This ticket requires special attention"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
|
||||||
|
msgid "Switch direction"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
|
||||||
|
msgid "Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
|
||||||
|
msgid "Scan a ticket or search and press return…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
|
||||||
|
msgid "Valid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
|
||||||
|
msgid "Canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Redeemed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Ticket not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
|
msgid "Additional information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
|
msgid "Valid ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
|
msgid "Exit recorded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
|
msgid "Ticket already used"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
|
msgid "Unknown ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
|
msgid "Ticket type not allowed here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
|
msgid "Entry not allowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket code revoked/changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
|
msgid "Ticket blocked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
|
msgid "Ticket not valid at this time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
|
msgid "Order canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
|
msgid "Ticket code is ambiguous on list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
|
msgid "Checked-in Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/lightbox/js/lightbox.js:96
|
||||||
|
msgid "close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||||
|
msgid ""
|
||||||
|
"Your request is currently being processed. Depending on the size of your "
|
||||||
|
"event, this might take up to a few minutes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||||
|
msgid "Your request has been queued on the server and will soon be processed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||||
|
msgid ""
|
||||||
|
"Your request arrived on the server but we still wait for it to be processed. "
|
||||||
|
"If this takes longer than two minutes, please contact us or go back in your "
|
||||||
|
"browser and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||||
|
msgid "An error of type {code} occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||||
|
"{code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||||
|
msgid "The request took too long. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||||
|
msgid "We are processing your request …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||||
|
msgid ""
|
||||||
|
"We are currently sending your request to the server. If this takes longer "
|
||||||
|
"than one minute, please check your internet connection and then reload this "
|
||||||
|
"page and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||||
|
msgid "Close message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:23
|
||||||
|
msgid "Copied!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:29
|
||||||
|
msgid "Press Ctrl-C to copy!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
|
||||||
|
msgid "is one of"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
|
||||||
|
msgid "is before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
|
||||||
|
msgid "is after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
|
||||||
|
msgid "Product"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
|
||||||
|
msgid "Product variation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
|
||||||
|
msgid "Current date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
|
||||||
|
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
|
||||||
|
msgid "Number of previous entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
|
||||||
|
msgid "Number of previous entries since midnight"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:83
|
||||||
|
msgid "Number of days with a previous entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
|
||||||
|
msgid "Minutes since last entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
|
||||||
|
msgid "Minutes since first entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||||
|
msgid "All of the conditions below (AND)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||||
|
msgid "At least one of the conditions below (OR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||||
|
msgid "Event start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||||
|
msgid "Event end"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||||
|
msgid "Event admission"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||||
|
msgid "custom date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||||
|
msgid "custom time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||||
|
msgid "Tolerance (minutes)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||||
|
msgid "Add condition"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||||
|
msgid "Duplicate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||||
|
msgid "Check-in QR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:387
|
||||||
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
|
msgid "Group of objects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:653
|
||||||
|
msgid "Text object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:655
|
||||||
|
msgid "Barcode area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:657
|
||||||
|
msgid "Image area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:659
|
||||||
|
msgid "Powered by pretix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:661
|
||||||
|
msgid "Object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:665
|
||||||
|
msgid "Ticket design"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:964
|
||||||
|
msgid "Saving failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1033
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1083
|
||||||
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1066
|
||||||
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:19
|
||||||
|
msgid "An error has occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:52
|
||||||
|
msgid "Generating messages …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:109
|
||||||
|
msgid "Unknown error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:311
|
||||||
|
msgid "Your color has great contrast and is very easy to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:315
|
||||||
|
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:319
|
||||||
|
msgid ""
|
||||||
|
"Your color has bad contrast for text on white background, please choose a "
|
||||||
|
"darker shade."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:468
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:488
|
||||||
|
msgid "Search query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:486
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:487
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||||
|
msgid "Selected only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||||
|
msgid "Use a different name internally"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:934
|
||||||
|
msgid "Click to close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||||
|
msgid "You have unsaved changes!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
|
||||||
|
msgid "Calculating default price…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||||
|
msgid "Others"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:82
|
||||||
|
msgid "Count"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
|
||||||
|
msgid "(one more date)"
|
||||||
|
msgid_plural "({num} more dates)"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||||
|
msgid ""
|
||||||
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
|
"complete your order as long as they’re available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||||
|
msgid "Cart expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
||||||
|
msgid "The items in your cart are reserved for you for one minute."
|
||||||
|
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||||
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||||
|
msgid "You get %(currency)s %(amount)s back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||||
|
msgid "Please enter the amount the organizer can keep."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:412
|
||||||
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:448
|
||||||
|
msgid "required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:551
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:570
|
||||||
|
msgid "Time zone:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:561
|
||||||
|
msgid "Your local time:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:39
|
||||||
|
msgid "Google Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:17
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:18
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Decrease quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:19
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Increase quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:20
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select variant %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Sold out"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Buy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Register"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Reserved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "FREE"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "from %(currency)s %(price)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "currently available: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Only available with a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "minimum amount to order: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The ticket shop could not be loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||||
|
"in a new tab to continue."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The cart could not be created. Please try again later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"We could not create your cart, since there are currently too many users in "
|
||||||
|
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Waiting list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"You currently have an active cart for this event. If you select more "
|
||||||
|
"products, they will be added to your existing cart."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Resume checkout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Voucher code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "See variations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different event"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open seat selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||||
|
"add yourself to the waiting list. We will then notify if seats are available "
|
||||||
|
"again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||||
|
msgid "Mo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||||
|
msgid "Tu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||||
|
msgid "We"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||||
|
msgid "Th"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||||
|
msgid "Fr"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||||
|
msgid "Sa"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||||
|
msgid "Su"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||||
|
msgid "January"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||||
|
msgid "February"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||||
|
msgid "March"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||||
|
msgid "April"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||||
|
msgid "May"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||||
|
msgid "June"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||||
|
msgid "July"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||||
|
msgid "August"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||||
|
msgid "September"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||||
|
msgid "October"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||||
|
msgid "November"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||||
|
msgid "December"
|
||||||
|
msgstr ""
|
||||||
+1846
-1588
File diff suppressed because it is too large
Load Diff
+1840
-1598
File diff suppressed because it is too large
Load Diff
+2029
-2153
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
|
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
|
||||||
"PO-Revision-Date: 2023-03-19 06:00+0000\n"
|
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"cs/>\n"
|
"cs/>\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||||
"X-Generator: Weblate 4.16.4\n"
|
"X-Generator: Weblate 5.0.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
@@ -251,7 +251,7 @@ msgstr "Vstupenka nebyla zaplacena. Chcete i přesto pokračovat?"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr "Je potřeba více infromací"
|
msgstr "Potřebné další informace"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
|
|||||||
+2039
-1837
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,969 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-09-06 08:12+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != "
|
||||||
|
"11) ? 2 : 3;\n"
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
||||||
|
msgid "Marked as paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||||
|
msgid "Comment:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||||
|
msgid "PayPal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
||||||
|
msgid "Venmo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:38
|
||||||
|
msgid "Apple Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
|
||||||
|
msgid "Itaú"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
|
||||||
|
msgid "PayPal Credit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
|
||||||
|
msgid "Credit Card"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
|
||||||
|
msgid "PayPal Pay Later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
|
||||||
|
msgid "iDEAL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||||
|
msgid "SEPA Direct Debit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
||||||
|
msgid "Bancontact"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
|
||||||
|
msgid "giropay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
|
||||||
|
msgid "SOFORT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
|
||||||
|
msgid "eps"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
|
||||||
|
msgid "MyBank"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
|
||||||
|
msgid "Przelewy24"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
|
||||||
|
msgid "Verkkopankki"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50
|
||||||
|
msgid "PayU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
|
||||||
|
msgid "BLIK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
|
||||||
|
msgid "Trustly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53
|
||||||
|
msgid "Zimpler"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54
|
||||||
|
msgid "Maxima"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55
|
||||||
|
msgid "OXXO"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56
|
||||||
|
msgid "Boleto"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
|
||||||
|
msgid "WeChat Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
|
||||||
|
msgid "Mercado Pago"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:164
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
|
||||||
|
msgid "Confirming your payment …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:247
|
||||||
|
msgid "Payment method unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Placed orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Paid orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||||
|
msgid "Total revenue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
|
||||||
|
msgid "Contacting Stripe …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
|
||||||
|
msgid "Total"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
|
||||||
|
msgid "Contacting your bank …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
|
||||||
|
msgid "Select a check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
|
||||||
|
msgid "No active check-in lists found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
|
||||||
|
msgid "Switch check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
|
||||||
|
msgid "Search results"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
|
||||||
|
msgid "No tickets found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
|
||||||
|
msgid "Result"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
|
||||||
|
msgid "This ticket requires special attention"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
|
||||||
|
msgid "Switch direction"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
|
||||||
|
msgid "Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
|
||||||
|
msgid "Scan a ticket or search and press return…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
|
||||||
|
msgid "Valid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
|
||||||
|
msgid "Canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Redeemed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Ticket not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
|
msgid "Additional information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
|
msgid "Valid ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
|
msgid "Exit recorded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
|
msgid "Ticket already used"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
|
msgid "Unknown ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
|
msgid "Ticket type not allowed here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
|
msgid "Entry not allowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket code revoked/changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
|
msgid "Ticket blocked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
|
msgid "Ticket not valid at this time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
|
msgid "Order canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
|
msgid "Ticket code is ambiguous on list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
|
msgid "Checked-in Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/lightbox/js/lightbox.js:96
|
||||||
|
msgid "close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||||
|
msgid ""
|
||||||
|
"Your request is currently being processed. Depending on the size of your "
|
||||||
|
"event, this might take up to a few minutes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||||
|
msgid "Your request has been queued on the server and will soon be processed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||||
|
msgid ""
|
||||||
|
"Your request arrived on the server but we still wait for it to be processed. "
|
||||||
|
"If this takes longer than two minutes, please contact us or go back in your "
|
||||||
|
"browser and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||||
|
msgid "An error of type {code} occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||||
|
"{code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||||
|
msgid "The request took too long. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||||
|
msgid "We are processing your request …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||||
|
msgid ""
|
||||||
|
"We are currently sending your request to the server. If this takes longer "
|
||||||
|
"than one minute, please check your internet connection and then reload this "
|
||||||
|
"page and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||||
|
msgid "Close message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:23
|
||||||
|
msgid "Copied!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:29
|
||||||
|
msgid "Press Ctrl-C to copy!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
|
||||||
|
msgid "is one of"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
|
||||||
|
msgid "is before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
|
||||||
|
msgid "is after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
|
||||||
|
msgid "Product"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
|
||||||
|
msgid "Product variation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
|
||||||
|
msgid "Current date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
|
||||||
|
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
|
||||||
|
msgid "Number of previous entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
|
||||||
|
msgid "Number of previous entries since midnight"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:83
|
||||||
|
msgid "Number of days with a previous entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
|
||||||
|
msgid "Minutes since last entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
|
||||||
|
msgid "Minutes since first entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||||
|
msgid "All of the conditions below (AND)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||||
|
msgid "At least one of the conditions below (OR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||||
|
msgid "Event start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||||
|
msgid "Event end"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||||
|
msgid "Event admission"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||||
|
msgid "custom date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||||
|
msgid "custom time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||||
|
msgid "Tolerance (minutes)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||||
|
msgid "Add condition"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||||
|
msgid "Duplicate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||||
|
msgid "Check-in QR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:387
|
||||||
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
|
msgid "Group of objects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:653
|
||||||
|
msgid "Text object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:655
|
||||||
|
msgid "Barcode area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:657
|
||||||
|
msgid "Image area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:659
|
||||||
|
msgid "Powered by pretix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:661
|
||||||
|
msgid "Object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:665
|
||||||
|
msgid "Ticket design"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:964
|
||||||
|
msgid "Saving failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1033
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1083
|
||||||
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1066
|
||||||
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:19
|
||||||
|
msgid "An error has occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:52
|
||||||
|
msgid "Generating messages …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:109
|
||||||
|
msgid "Unknown error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:311
|
||||||
|
msgid "Your color has great contrast and is very easy to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:315
|
||||||
|
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:319
|
||||||
|
msgid ""
|
||||||
|
"Your color has bad contrast for text on white background, please choose a "
|
||||||
|
"darker shade."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:468
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:488
|
||||||
|
msgid "Search query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:486
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:487
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||||
|
msgid "Selected only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||||
|
msgid "Use a different name internally"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:934
|
||||||
|
msgid "Click to close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||||
|
msgid "You have unsaved changes!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
|
||||||
|
msgid "Calculating default price…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||||
|
msgid "Others"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:82
|
||||||
|
msgid "Count"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
|
||||||
|
msgid "(one more date)"
|
||||||
|
msgid_plural "({num} more dates)"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||||
|
msgid ""
|
||||||
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
|
"complete your order as long as they’re available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||||
|
msgid "Cart expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
||||||
|
msgid "The items in your cart are reserved for you for one minute."
|
||||||
|
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||||
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||||
|
msgid "You get %(currency)s %(amount)s back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||||
|
msgid "Please enter the amount the organizer can keep."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:412
|
||||||
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:448
|
||||||
|
msgid "required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:551
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:570
|
||||||
|
msgid "Time zone:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:561
|
||||||
|
msgid "Your local time:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:39
|
||||||
|
msgid "Google Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:17
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:18
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Decrease quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:19
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Increase quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:20
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select variant %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Sold out"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Buy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Register"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Reserved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "FREE"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "from %(currency)s %(price)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "currently available: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Only available with a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "minimum amount to order: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The ticket shop could not be loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||||
|
"in a new tab to continue."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The cart could not be created. Please try again later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"We could not create your cart, since there are currently too many users in "
|
||||||
|
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Waiting list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"You currently have an active cart for this event. If you select more "
|
||||||
|
"products, they will be added to your existing cart."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Resume checkout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Voucher code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "See variations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different event"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open seat selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||||
|
"add yourself to the waiting list. We will then notify if seats are available "
|
||||||
|
"again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||||
|
msgid "Mo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||||
|
msgid "Tu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||||
|
msgid "We"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||||
|
msgid "Th"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||||
|
msgid "Fr"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||||
|
msgid "Sa"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||||
|
msgid "Su"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||||
|
msgid "January"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||||
|
msgid "February"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||||
|
msgid "March"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||||
|
msgid "April"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||||
|
msgid "May"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||||
|
msgid "June"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||||
|
msgid "July"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||||
|
msgid "August"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||||
|
msgid "September"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||||
|
msgid "October"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||||
|
msgid "November"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||||
|
msgid "December"
|
||||||
|
msgstr ""
|
||||||
+1836
-1593
File diff suppressed because it is too large
Load Diff
+1828
-1595
File diff suppressed because it is too large
Load Diff
@@ -367,6 +367,7 @@ XXXXX
|
|||||||
Yubikey
|
Yubikey
|
||||||
Zahlungs
|
Zahlungs
|
||||||
Zahlungsbestätigungs
|
Zahlungsbestätigungs
|
||||||
|
Zahlungserinnerungs
|
||||||
zahlungspflichtig
|
zahlungspflichtig
|
||||||
Zahlungsplugins
|
Zahlungsplugins
|
||||||
Zehnerkarten
|
Zehnerkarten
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -367,6 +367,7 @@ XXXXX
|
|||||||
Yubikey
|
Yubikey
|
||||||
Zahlungs
|
Zahlungs
|
||||||
Zahlungsbestätigungs
|
Zahlungsbestätigungs
|
||||||
|
Zahlungserinnerungs
|
||||||
zahlungspflichtig
|
zahlungspflichtig
|
||||||
Zahlungsplugins
|
Zahlungsplugins
|
||||||
Zehnerkarten
|
Zehnerkarten
|
||||||
|
|||||||
+1800
-1600
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-07-27 11:50+0000\n"
|
"POT-Creation-Date: 2023-09-08 13:36+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|||||||
+1850
-1592
File diff suppressed because it is too large
Load Diff
+1800
-1600
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,968 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-09-06 08:12+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
||||||
|
msgid "Marked as paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||||
|
msgid "Comment:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||||
|
msgid "PayPal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
||||||
|
msgid "Venmo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:38
|
||||||
|
msgid "Apple Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
|
||||||
|
msgid "Itaú"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
|
||||||
|
msgid "PayPal Credit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
|
||||||
|
msgid "Credit Card"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
|
||||||
|
msgid "PayPal Pay Later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
|
||||||
|
msgid "iDEAL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||||
|
msgid "SEPA Direct Debit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
||||||
|
msgid "Bancontact"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
|
||||||
|
msgid "giropay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
|
||||||
|
msgid "SOFORT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
|
||||||
|
msgid "eps"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
|
||||||
|
msgid "MyBank"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
|
||||||
|
msgid "Przelewy24"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
|
||||||
|
msgid "Verkkopankki"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:50
|
||||||
|
msgid "PayU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
|
||||||
|
msgid "BLIK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
|
||||||
|
msgid "Trustly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:53
|
||||||
|
msgid "Zimpler"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:54
|
||||||
|
msgid "Maxima"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:55
|
||||||
|
msgid "OXXO"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:56
|
||||||
|
msgid "Boleto"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
|
||||||
|
msgid "WeChat Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
|
||||||
|
msgid "Mercado Pago"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:164
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
|
||||||
|
msgid "Confirming your payment …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:247
|
||||||
|
msgid "Payment method unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Placed orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||||
|
msgid "Paid orders"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
|
||||||
|
msgid "Total revenue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
|
||||||
|
msgid "Contacting Stripe …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
|
||||||
|
msgid "Total"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
|
||||||
|
msgid "Contacting your bank …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
|
||||||
|
msgid "Select a check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
|
||||||
|
msgid "No active check-in lists found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
|
||||||
|
msgid "Switch check-in list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
|
||||||
|
msgid "Search results"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
|
||||||
|
msgid "No tickets found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
|
||||||
|
msgid "Result"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
|
||||||
|
msgid "This ticket requires special attention"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
|
||||||
|
msgid "Switch direction"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
|
||||||
|
msgid "Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
|
||||||
|
msgid "Exit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
|
||||||
|
msgid "Scan a ticket or search and press return…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
|
||||||
|
msgid "Valid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
|
||||||
|
msgid "Canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Redeemed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Ticket not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
|
msgid "Additional information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
|
msgid "Valid ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
|
msgid "Exit recorded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
|
msgid "Ticket already used"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
|
msgid "Unknown ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
|
msgid "Ticket type not allowed here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
|
msgid "Entry not allowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket code revoked/changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
|
msgid "Ticket blocked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
|
msgid "Ticket not valid at this time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
|
msgid "Order canceled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
|
msgid "Ticket code is ambiguous on list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
|
msgid "Checked-in Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/lightbox/js/lightbox.js:96
|
||||||
|
msgid "close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||||
|
msgid ""
|
||||||
|
"Your request is currently being processed. Depending on the size of your "
|
||||||
|
"event, this might take up to a few minutes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||||
|
msgid "Your request has been queued on the server and will soon be processed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||||
|
msgid ""
|
||||||
|
"Your request arrived on the server but we still wait for it to be processed. "
|
||||||
|
"If this takes longer than two minutes, please contact us or go back in your "
|
||||||
|
"browser and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||||
|
msgid "An error of type {code} occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||||
|
"{code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||||
|
msgid "The request took too long. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||||
|
msgid ""
|
||||||
|
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||||
|
msgid "We are processing your request …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||||
|
msgid ""
|
||||||
|
"We are currently sending your request to the server. If this takes longer "
|
||||||
|
"than one minute, please check your internet connection and then reload this "
|
||||||
|
"page and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||||
|
msgid "Close message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:23
|
||||||
|
msgid "Copied!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/clipboard.js:29
|
||||||
|
msgid "Press Ctrl-C to copy!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
|
||||||
|
msgid "is one of"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
|
||||||
|
msgid "is before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
|
||||||
|
msgid "is after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
|
||||||
|
msgid "Product"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
|
||||||
|
msgid "Product variation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
|
||||||
|
msgid "Current date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
|
||||||
|
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
|
||||||
|
msgid "Number of previous entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
|
||||||
|
msgid "Number of previous entries since midnight"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:83
|
||||||
|
msgid "Number of days with a previous entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
|
||||||
|
msgid "Minutes since last entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
|
||||||
|
msgid "Minutes since first entry (-1 on first entry)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||||
|
msgid "All of the conditions below (AND)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||||
|
msgid "At least one of the conditions below (OR)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||||
|
msgid "Event start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||||
|
msgid "Event end"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||||
|
msgid "Event admission"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||||
|
msgid "custom date and time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||||
|
msgid "custom time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||||
|
msgid "Tolerance (minutes)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||||
|
msgid "Add condition"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||||
|
msgid "Duplicate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||||
|
msgid "Check-in QR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:387
|
||||||
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
|
msgid "Group of objects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:653
|
||||||
|
msgid "Text object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:655
|
||||||
|
msgid "Barcode area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:657
|
||||||
|
msgid "Image area"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:659
|
||||||
|
msgid "Powered by pretix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:661
|
||||||
|
msgid "Object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:665
|
||||||
|
msgid "Ticket design"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:964
|
||||||
|
msgid "Saving failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1033
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1083
|
||||||
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1066
|
||||||
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:19
|
||||||
|
msgid "An error has occurred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/mail.js:52
|
||||||
|
msgid "Generating messages …"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:109
|
||||||
|
msgid "Unknown error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:311
|
||||||
|
msgid "Your color has great contrast and is very easy to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:315
|
||||||
|
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:319
|
||||||
|
msgid ""
|
||||||
|
"Your color has bad contrast for text on white background, please choose a "
|
||||||
|
"darker shade."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:468
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:488
|
||||||
|
msgid "Search query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:486
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:487
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||||
|
msgid "Selected only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||||
|
msgid "Use a different name internally"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:934
|
||||||
|
msgid "Click to close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||||
|
msgid "You have unsaved changes!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
|
||||||
|
msgid "Calculating default price…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||||
|
msgid "Others"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:82
|
||||||
|
msgid "Count"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
|
||||||
|
msgid "(one more date)"
|
||||||
|
msgid_plural "({num} more dates)"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||||
|
msgid ""
|
||||||
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
|
"complete your order as long as they’re available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||||
|
msgid "Cart expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
||||||
|
msgid "The items in your cart are reserved for you for one minute."
|
||||||
|
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||||
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||||
|
msgid "You get %(currency)s %(amount)s back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||||
|
msgid "Please enter the amount the organizer can keep."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:412
|
||||||
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:448
|
||||||
|
msgid "required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:551
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:570
|
||||||
|
msgid "Time zone:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/ui/main.js:561
|
||||||
|
msgid "Your local time:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/walletdetection.js:39
|
||||||
|
msgid "Google Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:17
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:18
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Decrease quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:19
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Increase quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:20
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Select variant %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Sold out"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Buy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Register"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Reserved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "FREE"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "from %(currency)s %(price)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus %(rate)s% %(taxname)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "incl. taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "plus taxes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "currently available: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Only available with a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||||
|
#, javascript-format
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "minimum amount to order: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The ticket shop could not be loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||||
|
"in a new tab to continue."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open ticket shop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "The cart could not be created. Please try again later"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"We could not create your cart, since there are currently too many users in "
|
||||||
|
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Waiting list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"You currently have an active cart for this event. If you select more "
|
||||||
|
"products, they will be added to your existing cart."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Resume checkout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem a voucher"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Redeem"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Voucher code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Continue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "See variations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different event"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Choose a different date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Next week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Previous week"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Open seat selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid ""
|
||||||
|
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||||
|
"add yourself to the waiting list. We will then notify if seats are available "
|
||||||
|
"again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||||
|
msgctxt "widget"
|
||||||
|
msgid "Load more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||||
|
msgid "Mo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||||
|
msgid "Tu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||||
|
msgid "We"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||||
|
msgid "Th"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||||
|
msgid "Fr"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||||
|
msgid "Sa"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||||
|
msgid "Su"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||||
|
msgid "January"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||||
|
msgid "February"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||||
|
msgid "March"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||||
|
msgid "April"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||||
|
msgid "May"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||||
|
msgid "June"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||||
|
msgid "July"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||||
|
msgid "August"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||||
|
msgid "September"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||||
|
msgid "October"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||||
|
msgid "November"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||||
|
msgid "December"
|
||||||
|
msgstr ""
|
||||||
+1874
-1644
File diff suppressed because it is too large
Load Diff
+1841
-1609
File diff suppressed because it is too large
Load Diff
+1854
-1614
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: French\n"
|
"Project-Id-Version: French\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
|
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
|
||||||
"PO-Revision-Date: 2023-08-02 02:00+0000\n"
|
"PO-Revision-Date: 2023-09-11 10:00+0000\n"
|
||||||
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
|
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
|
||||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"fr/>\n"
|
"fr/>\n"
|
||||||
@@ -63,7 +63,7 @@ msgstr "iDEAL"
|
|||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||||
msgid "SEPA Direct Debit"
|
msgid "SEPA Direct Debit"
|
||||||
msgstr "Prélèvement SEPA"
|
msgstr "Mandat SEPA"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
||||||
msgid "Bancontact"
|
msgid "Bancontact"
|
||||||
|
|||||||
+1827
-1593
File diff suppressed because it is too large
Load Diff
+1800
-1600
File diff suppressed because it is too large
Load Diff
+1800
-1600
File diff suppressed because it is too large
Load Diff
+1814
-1597
File diff suppressed because it is too large
Load Diff
+9544
-5923
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user