Compare commits

..
Author SHA1 Message Date
Raphael Michel bb82d013e6 OrderChangeManager: Prevent usage on canceled orders 2023-01-24 18:54:46 +01:00
194 changed files with 60757 additions and 66111 deletions
+2 -2
View File
@@ -26,10 +26,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+4 -4
View File
@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -50,10 +50,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -45,10 +45,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -66,10 +66,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.11
python-version: 3.9
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders
+6 -6
View File
@@ -24,22 +24,22 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.7", "3.9", "3.10"]
database: [sqlite, postgres, mysql]
exclude:
- database: mysql
python-version: "3.9"
python-version: "3.10"
- database: mysql
python-version: "3.11"
- database: sqlite
python-version: "3.9"
- database: sqlite
python-version: "3.7"
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.10'
mariadb version: '10.4'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
@@ -83,4 +83,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
if: matrix.database == 'postgres' && matrix.python-version == '3.10'
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11-bullseye
FROM python:3.9-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
+4 -4
View File
@@ -141,7 +141,7 @@ Database settings
Example::
[database]
backend=postgresql
backend=mysql
name=pretix
user=pretix
password=abcd
@@ -149,7 +149,7 @@ Example::
port=3306
``backend``
One of ``mysql`` (deprecated), ``sqlite3`` and ``postgresql``.
One of ``mysql``, ``sqlite3``, ``oracle`` and ``postgresql``.
Default: ``sqlite3``.
If you use MySQL, be sure to create your database using
@@ -163,7 +163,7 @@ Example::
Connection details for the database connection. Empty by default.
``galera``
(Deprecated) Indicates if the database backend is a MySQL/MariaDB Galera cluster and
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`:
@@ -194,7 +194,7 @@ Example::
[urls]
media=/media/
static=/static/
static=/media/
``media``
The URL to be used to serve user-uploaded content. You should not need to modify
-1
View File
@@ -14,5 +14,4 @@ This documentation is for everyone who wants to install pretix on a server.
maintainance
scaling
errors
mysql2postgres
indexes
+18 -3
View File
@@ -14,7 +14,7 @@ This has some trade-offs in terms of performance and isolation but allows a rath
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 11.0** but it should work very similar on other
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones.
Requirements
@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+ database server
* A `PostgreSQL`_ 9.6+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -58,6 +58,9 @@ directory writable to the user that runs pretix inside the docker container::
Database
--------
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
our database's shell. Please make sure that UTF8 is used as encoding for the best compatibility. You can check this with
the following command::
@@ -83,6 +86,13 @@ Restart PostgreSQL after you changed these files::
If you have a firewall running, you should also make sure that port 5432 is reachable from the ``172.17.0.1/16`` subnet.
For MySQL, you can either also use network-based connections or mount the ``/var/run/mysqld/mysqld.sock`` socket into the docker container.
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
Redis
-----
@@ -142,13 +152,15 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
trust_x_forwarded_proto=on
[database]
; Replace postgresql with mysql for MySQL
backend=postgresql
name=pretix
user=pretix
; Replace with the password you chose above
password=*********
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjust
; this to wherever your database is running, e.g. the name of a linked container.
; this to wherever your database is running, e.g. the name of a linked container
; or of a mounted MySQL socket.
host=172.17.0.1
[mail]
@@ -200,6 +212,8 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
[Install]
WantedBy=multi-user.target
When using MySQL and socket mounting, you'll need the additional flag ``-v /var/run/mysqld:/var/run/mysqld`` in the command.
You can now run the following commands
to enable and start the service::
@@ -325,6 +339,7 @@ workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
+23 -5
View File
@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 11+ database server
* A `PostgreSQL`_ 9.6+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
* A `nodejs`_ installation
@@ -47,6 +47,9 @@ In this guide, all code lines prepended with a ``#`` symbol are commands that yo
Database
--------
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
Having the database server installed, we still need a database and a database user. We can create these with any kind
of database managing tool or directly on our database's shell. Please make sure that UTF8 is used as encoding for the
best compatibility. You can check this with the following command::
@@ -58,6 +61,12 @@ For PostgreSQL database creation, we would do::
# sudo -u postgres createuser pretix
# sudo -u postgres createdb -O pretix pretix
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
Package dependencies
--------------------
@@ -65,7 +74,7 @@ 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 \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
gettext libpq-dev libmariadb-dev libjpeg-dev libopenjp2-7-dev
Config file
-----------
@@ -88,12 +97,16 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
trust_x_forwarded_proto=on
[database]
; For MySQL, replace with "mysql"
backend=postgresql
name=pretix
user=pretix
; For PostgreSQL on the same host, we don't need a password because we can use
; peer authentication if our PostgreSQL user matches our unix user.
; For MySQL, enter the user password. For PostgreSQL on the same host,
; we don't need one because we can use peer authentification if our
; PostgreSQL user matches our unix user.
password=
; For MySQL, use local socket, e.g. /var/run/mysqld/mysqld.sock
; For a remote host, supply an IP address
; For local postgres authentication, you can leave it empty
host=
@@ -127,7 +140,11 @@ We now install pretix, its direct dependencies and gunicorn::
(venv)$ pip3 install pretix gunicorn
Note that you need Python 3.9 or newer. You can find out your Python version using ``python -V``.
If you're running MySQL, also install the client library::
(venv)$ pip3 install mysqlclient
Note that you need Python 3.7 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::
@@ -327,6 +344,7 @@ Then, proceed like after any plugin installation::
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
+5 -5
View File
@@ -17,11 +17,11 @@ Backups
There are essentially two things which you should create backups of:
Database
Your SQL database. This is critical and you should **absolutely always create automatic
backups of your database**. There are tons of tutorials on the internet on how to do this,
and the exact process depends on the choice of your database. For PostgreSQL, see the
``pg_dump`` tool. You probably want to create a cronjob that does the backups for you on a
regular schedule.
Your SQL database (MySQL or PostgreSQL). This is critical and you should **absolutely
always create automatic backups of your database**. There are tons of tutorials on the
internet on how to do this, and the exact process depends on the choice of your database.
For MySQL, see ``mysqldump`` and for PostgreSQL, see the ``pg_dump`` tool. You probably
want to create a cronjob that does the backups for you on a regular schedule.
Data directory
The data directory of your pretix configuration might contain some things that you should
-156
View File
@@ -1,156 +0,0 @@
.. highlight:: none
Migrating from MySQL/MariaDB to PostgreSQL
==========================================
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB will be removed in
pretix 5.0.
In order to follow this guide, your pretix installation needs to be a version that fully supports MySQL/MariaDB. If you
already upgraded to pretix 5.0, downgrade back to the last 4.x release using ``pip``.
.. note:: We have tested this guide carefully, but we can't assume any liability for its correctness. The data loss
risk should be low as long as pretix is not running while you do the migration. If you are a pretix Enterprise
customer, feel free to reach out in advance if you want us to support you along the way.
Update database schema
----------------------
Before you start, make sure your database schema is up to date::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ python -m pretix migrate
Install PostgreSQL
------------------
Now, install and set up a PostgreSQL server. For a local installation on Debian or Ubuntu, use::
# apt install postgresql
Having the database server installed, we still need a database and a database user. We can create these with any kind
of database managing tool or directly on our database's shell. Please make sure that UTF8 is used as encoding for the
best compatibility. You can check this with the following command::
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
Without Docker
""""""""""""""
For our standard manual installation, create the database and user like this::
# sudo -u postgres createuser pretix
# sudo -u postgres createdb -O pretix pretix
With Docker
"""""""""""
For our standard docker installation, create the database and user like this::
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
listen_addresses = 'localhost,172.17.0.1'
You also need to add a new line to ``/etc/postgresql/<version>/main/pg_hba.conf`` to allow network connections to this user and database::
host pretix pretix 172.17.0.1/16 md5
Restart PostgreSQL after you changed these files::
# systemctl restart postgresql
If you have a firewall running, you should also make sure that port 5432 is reachable from the ``172.17.0.1/16`` subnet.
Of course, instead of all this you can also run a PostgreSQL docker container and link it to the pretix container.
Stop pretix
-----------
To prevent any more changes to your data, stop pretix from running::
# systemctl stop pretix-web pretix-worker
Change configuration
--------------------
Change the database configuration in your ``/etc/pretix/pretix.cfg`` file::
[database]
backend=postgresql
name=pretix
user=pretix
password= ; only required for docker or remote database, can be kept empty for local auth
host= ; set to 172.17.0.1 in docker setup, keep empty for local auth
Create database schema
-----------------------
To create the schema in your new PostgreSQL database, use the following commands::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ python -m pretix migrate
Migrate your data
-----------------
Install ``pgloader``::
# apt install pgloader
.. note::
If you are using Ubuntu 20.04, the ``pgloader`` version from the repositories seems to be incompatible with PostgreSQL
12+. You can install ``pgloader`` from the `PostgreSQL repositories`_ instead.
See also `this discussion <https://github.com/pretix/pretix/issues/3090>`_.
Create a new file ``/tmp/pretix.load``, replacing the MySQL and PostgreSQL connection strings with the correct user names, passwords, and/or database names::
LOAD DATABASE
FROM mysql://pretix:password@localhost/pretix -- replace with mysql://username:password@hostname/dbname
INTO postgresql:///pretix -- replace with dbname
WITH data only, include no drop, truncate, disable triggers,
create no indexes, drop indexes, reset sequences
ALTER SCHEMA 'pretix' RENAME TO 'public' -- replace pretix with the name of the MySQL database
ALTER TABLE NAMES MATCHING ~/.*/
SET SCHEMA 'public'
SET timezone TO '+00:00'
SET PostgreSQL PARAMETERS
maintenance_work_mem to '128MB',
work_mem to '12MB';
Then, run::
# sudo -u postgres pgloader /tmp/pretix.load
The output should end with a table summarizing the results for every table. You can ignore warnings about type casts
and missing constraints.
Afterwards, delete the file again::
# rm -rf /tmp/pretix.load
Start pretix
------------
Now, restart pretix. Maybe stop your MySQL server as a verification step that you are no longer using it::
# systemctl stop mariadb
# systemctl start pretix-web pretix-worker
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.
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt
+2 -2
View File
@@ -42,7 +42,7 @@ A pretix installation usually consists of the following components which run per
* ``pretix-worker`` is a Celery-based application that processes tasks that should be run asynchronously outside of the web application process.
* A **PostgreSQL database** keeps all the important data and processes the actual transactions.
* A **SQL database** keeps all the important data and processes the actual transactions. We recommend using PostgreSQL, but MySQL/MariaDB works as well.
* A **web server** that terminates TLS and HTTP connections and forwards them to ``pretix-web``. In some cases, e.g. when serving static files, the web servers might return a response directly. We recommend using ``nginx``.
@@ -74,7 +74,7 @@ We recommend reading up on tuning your web server for high concurrency. For ngin
processes and the number of connections each worker process accepts. Double-check that TLS session caching works, because TLS
handshakes can get really expensive.
During a traffic peak, your web server will be able to make use of more CPU resources, while memory usage will stay comparatively low,
During a traffic peak, your web server will be able to make us of more CPU resources, while memory usage will stay comparatively low,
so if you invest in more hardware here, invest in more and faster CPU cores.
Make sure that pretix' static files (such as CSS and JavaScript assets) as well as user-uploaded media files (event logos, etc)
-3
View File
@@ -42,7 +42,6 @@ introductory_text string Text to be prin
additional_text string Text to be printed below the product list
payment_provider_text string Text to be printed below the product list with
payment information
payment_provider_stamp string Short text to be visibly printed to indicate payment status
footer_text string Text to be printed in the page footer area
lines list of objects The actual invoice contents
├ position integer Number of the line within an invoice.
@@ -179,7 +178,6 @@ Endpoints
"internal_reference": "",
"additional_text": "We are looking forward to see you on our conference!",
"payment_provider_text": "Please transfer the money to our account ABC…",
"payment_provider_stamp": null,
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
@@ -270,7 +268,6 @@ Endpoints
"internal_reference": "",
"additional_text": "We are looking forward to see you on our conference!",
"payment_provider_text": "Please transfer the money to our account ABC…",
"payment_provider_stamp": null,
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
+5 -12
View File
@@ -26,7 +26,6 @@ limit_events list of strings If ``all_events
action_types list of strings A list of action type filters that limit the
notifications sent to this webhook. See below for
valid values
comment string Internal comment on this webhook, default ``null``
===================================== ========================== =======================================================
The following values for ``action_types`` are valid with pretix core:
@@ -57,7 +56,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.subevent.added``
* ``pretix.subevent.changed``
* ``pretix.subevent.deleted``
* ``pretix.event.item.*``
* ``pretix.event.live.activated``
* ``pretix.event.live.deactivated``
* ``pretix.event.testmode.activated``
@@ -100,8 +98,7 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
]
}
@@ -138,8 +135,7 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -166,8 +162,7 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": "Called for changes"
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
**Example response**:
@@ -184,8 +179,7 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": "Called for changes"
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to create a webhook for
@@ -230,8 +224,7 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
}
:param organizer: The ``slug`` field of the organizer to modify
-2
View File
@@ -102,8 +102,6 @@ The provider class
.. automethod:: render_invoice_text
.. automethod:: render_invoice_stamp
.. automethod:: order_change_allowed
.. automethod:: payment_prepare
+1 -1
View File
@@ -19,4 +19,4 @@
# 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/>.
#
__version__ = "4.17.0.dev0"
__version__ = "4.16.0.dev0"
+1 -2
View File
@@ -32,7 +32,6 @@ from rest_framework import status
from pretix.api.models import ApiCall
from pretix.base.models import Organizer
from pretix.helpers import OF_SELF
class IdempotencyMiddleware:
@@ -57,7 +56,7 @@ class IdempotencyMiddleware:
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
call, created = ApiCall.objects.select_for_update(of=OF_SELF).get_or_create(
call, created = ApiCall.objects.select_for_update().get_or_create(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
defaults={
@@ -1,18 +0,0 @@
# Generated by Django 3.2.17 on 2023-02-07 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0009_auto_20221217_1847'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='comment',
field=models.CharField(max_length=255, null=True),
),
]
-1
View File
@@ -112,7 +112,6 @@ class WebHook(models.Model):
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
class Meta:
ordering = ('id',)
+3 -3
View File
@@ -1484,9 +1484,9 @@ class InvoiceSerializer(I18nAwareModelSerializer):
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
'foreign_currency_rate_date', 'internal_reference')
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference')
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
+1 -1
View File
@@ -55,7 +55,7 @@ class WebHookSerializer(I18nAwareModelSerializer):
class Meta:
model = WebHook
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types', 'comment')
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
def validate(self, data):
data = super().validate(data)
+2 -2
View File
@@ -95,7 +95,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
filterset_class = CheckinListFilter
ordering = ('subevent__date_from', 'name', 'id')
ordering = ('subevent__date_from', 'name')
ordering_fields = ('subevent__date_from', 'id', 'name',)
def _get_permission_name(self, request):
@@ -684,7 +684,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (ExtendedBackend, RichOrderingFilter)
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'pk')
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
+2 -3
View File
@@ -51,7 +51,6 @@ from pretix.base.models import (
User,
)
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
from pretix.presale.style import regenerate_organizer_css
@@ -179,7 +178,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
@@ -197,7 +196,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=["POST"])
@transaction.atomic()
def transact(self, request, **kwargs):
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
gc = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value')
)
+29 -28
View File
@@ -42,7 +42,6 @@ from pretix.base.models import LogEntry
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import periodic_task
from pretix.celery_app import app
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__)
_ALL_EVENTS = None
@@ -96,7 +95,7 @@ def get_all_webhook_events():
return types
class ParametrizedWebhookEvent(WebhookEvent):
class ParametrizedOrderWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
@@ -110,8 +109,6 @@ class ParametrizedWebhookEvent(WebhookEvent):
def verbose_name(self):
return self._verbose_name
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
order = logentry.content_object
if not order:
@@ -126,7 +123,19 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
class ParametrizedEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
if logentry.action_type == 'pretix.event.deleted':
@@ -150,7 +159,19 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
class ParametrizedSubEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
@@ -163,19 +184,6 @@ class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'item': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -296,11 +304,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.subevent.deleted',
pgettext_lazy('subevent', 'Event series date deleted'),
),
ParametrizedItemWebhookEvent(
'pretix.event.item.*',
_('Product changed (including product added or deleted and including changes to nested objects like '
'variations or bundles)'),
),
ParametrizedEventWebhookEvent(
'pretix.event.live.activated',
_('Shop taken live'),
@@ -499,8 +502,7 @@ def manually_retry_all_calls(webhook_id: int):
webhook = WebHook.objects.get(id=webhook_id)
with scope(organizer=webhook.organizer), transaction.atomic():
for whcr in webhook.retries.select_for_update(
skip_locked=connection.features.has_select_for_update_skip_locked,
of=OF_SELF
skip_locked=connection.features.has_select_for_update_skip_locked
):
send_webhook.apply_async(
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
@@ -513,8 +515,7 @@ def manually_retry_all_calls(webhook_id: int):
def schedule_webhook_retries_on_celery(sender, **kwargs):
with transaction.atomic():
for whcr in WebHookCallRetry.objects.select_for_update(
skip_locked=connection.features.has_select_for_update_skip_locked,
of=OF_SELF
skip_locked=connection.features.has_select_for_update_skip_locked
).filter(retry_not_before__lt=now()):
send_webhook.apply_async(
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
+8 -42
View File
@@ -35,8 +35,8 @@ from django.utils.formats import date_format, localize
from django.utils.translation import (
get_language, gettext, gettext_lazy, pgettext,
)
from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
@@ -44,8 +44,8 @@ from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
PageTemplate, Paragraph, Spacer, Table, TableStyle,
BaseDocTemplate, Frame, KeepTogether, NextPageTemplate, PageTemplate,
Paragraph, Spacer, Table, TableStyle,
)
from pretix.base.decimal import round_decimal
@@ -147,8 +147,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER))
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
@@ -251,31 +249,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
class PaidMarker(Flowable):
def __init__(self, text='paid', color=None, font='OpenSansBd', size=20):
super().__init__()
self.text = text
self.color = color
self.font = font
self.size = size
self._showBoundary = True
def wrap(self, availwidth, availheight):
# Fake a size, we don't care if we exceed the table
return 10, self.size / 2
def draw(self):
self.canv.translate(0, - self.size / 2)
self.canv.rotate(2)
self.canv.setFont(self.font, self.size)
self.canv.setFillColor(self.color)
width = self.canv.stringWidth(self.text, self.font, self.size)
self.canv.drawRightString(0, 0, self.text)
self.canv.setStrokeColor(self.color)
self.canv.roundRect(-width - self.size / 2, -self.size / 4, width + self.size, self.size + self.size / 4, 3)
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
@@ -639,10 +612,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tdata.append([
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
colwidths = [a * doc.width for a in (.65, .05, .30)]
if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation:
if self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
@@ -654,7 +627,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.event.settings.invoice_show_payments and self.invoice.order.payments.filter(
elif self.invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED), provider='giftcard'
).exists():
giftcard_sum = self.invoice.order.payments.filter(
@@ -672,13 +645,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.payment_provider_stamp:
pm = PaidMarker(
text=self.invoice.payment_provider_stamp,
color=colors.HexColor(self.event.settings.theme_color_success),
size=16
)
tdata[-1][-2] = pm
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
@@ -1,19 +0,0 @@
# Generated by Django 3.2.17 on 2023-02-07 10:00
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0228_scheduledeventexport_scheduledorganizerexport'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='payment_provider_stamp',
field=models.CharField(max_length=100, null=True),
),
]
+55 -66
View File
@@ -35,11 +35,10 @@ from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db import connection, models
from django.db.models import (
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
)
from django.db.models.expressions import RawSQL
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
@@ -99,18 +98,15 @@ class CheckinList(LoggedModel):
class Meta:
ordering = ('subevent__date_from', 'name')
def positions_query(self, ignore_status=False):
@property
def positions(self):
from . import Order, OrderPosition
qs = OrderPosition.all.filter(
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [
Order.STATUS_PAID],
)
if not ignore_status:
qs = qs.filter(
canceled=False,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
)
if self.subevent_id:
qs = qs.filter(subevent_id=self.subevent_id)
if not self.all_products:
@@ -118,46 +114,38 @@ class CheckinList(LoggedModel):
return qs
@property
def positions(self):
return self.positions_query(ignore_status=False)
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
if at_time is None:
c_q = []
else:
c_q = [Q(datetime__lt=at_time)]
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use a simple approach that works on all databases
qs = self.positions_query(ignore_status=ignore_status).annotate(
last_entry=Subquery(
Checkin.objects.filter(
*c_q,
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_ENTRY,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
last_exit=Subquery(
Checkin.objects.filter(
*c_q,
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_EXIT,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
).filter(
Q(last_entry__isnull=False)
& Q(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
def positions_inside(self):
return self.positions.annotate(
last_entry=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_ENTRY,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
last_exit=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_EXIT,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
).filter(
Q(last_entry__isnull=False)
& Q(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
return qs
)
@property
def inside_count(self):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use the simple query that works on all databases
return self.positions_inside.count()
# Use the PostgreSQL-specific query using Window functions, which is a lot faster.
# On a real-world example with ~100k tickets, of which ~17k are checked in, we observed
@@ -169,7 +157,7 @@ class CheckinList(LoggedModel):
# dedupliate by position and count it up.
cl = self
base_q, base_params = (
Checkin.all.filter(*c_q, successful=True, list=cl)
Checkin.all.filter(successful=True, position__in=cl.positions, list=cl)
.annotate(
cnt_exists_after=Window(
expression=Count("position_id", filter=Q(type=Value("exit"))),
@@ -183,25 +171,26 @@ class CheckinList(LoggedModel):
.values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params()
)
return self.positions_query(ignore_status=ignore_status).filter(
pk__in=RawSQL(
with connection.cursor() as cursor:
cursor.execute(
f"""
SELECT "position_id"
FROM ({str(base_q)}) s
WHERE "type" = %s AND "cnt_exists_after" = 0
GROUP BY "position_id"
SELECT COUNT(*) FROM (
SELECT COUNT("position_id")
FROM ({str(base_q)} ) s
WHERE "type" = %s AND "cnt_exists_after" = 0
GROUP BY "position_id"
) a;
""",
[*base_params, Checkin.TYPE_ENTRY]
[
*base_params,
"entry",
],
)
)
@property
def positions_inside(self):
return self.positions_inside_query(None)
@property
def inside_count(self):
return self.positions_inside_query(None).count()
rows = cursor.fetchall()
if rows:
return rows[0][0]
return 0
@property
@scopes_disabled()
+41 -82
View File
@@ -36,7 +36,7 @@ import logging
import os
import string
import uuid
from collections import Counter, OrderedDict, defaultdict
from collections import OrderedDict
from datetime import datetime, time, timedelta
from operator import attrgetter
from urllib.parse import urljoin
@@ -340,104 +340,64 @@ class EventMixin:
)
)
@property
def best_availability_state(self):
return self.best_availability[0]
@property
def best_availability_is_low(self):
"""
Returns ``True`` if the availability of tickets in this event is lower than the percentage
given in setting ``low_availability_percentage``.
"""
if not self.settings.low_availability_percentage:
return False
ba = self.best_availability
if ba[1] is None or not ba[2]:
return False
percentage = ba[1] / ba[2] * 100
return percentage < self.settings.low_availability_percentage
@cached_property
def best_availability(self):
"""
Returns a 3-tuple of
- The availability state of this event (one of the ``Quota.AVAILABILITY_*`` constants)
- The number of tickets currently available (or ``None``)
- The number of tickets "originally" available (or ``None``)
This can only be called on objects obtained through a queryset that has been passed through ``.annotated()``.
"""
def best_availability_state(self):
from .items import Quota
if not hasattr(self, 'active_quotas'):
raise TypeError("Call this only if you fetched the subevents via Event/SubEvent.annotated()")
items_available = set()
vars_available = set()
items_reserved = set()
vars_reserved = set()
items_gone = set()
vars_gone = set()
items_disabled = set()
vars_disabled = set()
if hasattr(self, 'disabled_items'): # SubEventItem
items_disabled = set(self.disabled_items.split(","))
else:
items_disabled = set()
if hasattr(self, 'disabled_vars'): # SubEventItemVariation
vars_disabled = set(self.disabled_vars.split(","))
else:
vars_disabled = set()
# Compute the availability of all quotas and build a item→quotas mapping with all non-disabled items
r = getattr(self, '_quota_cache', {})
quotas_for_item = defaultdict(list)
quotas_for_variation = defaultdict(list)
for q in self.active_quotas:
if q not in r:
r[q] = q.availability(allow_cache=True)
res = r[q] if q in r else q.availability(allow_cache=True)
if q.active_items:
for item_id in q.active_items.split(","):
if item_id not in items_disabled:
quotas_for_item[item_id].append(q)
if q.active_variations:
for var_id in q.active_variations.split(","):
if var_id not in vars_disabled:
quotas_for_variation[var_id].append(q)
if res[0] == Quota.AVAILABILITY_OK:
if q.active_items:
items_available.update(q.active_items.split(","))
if q.active_variations:
vars_available.update(q.active_variations.split(","))
elif res[0] == Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_reserved.update(q.active_items.split(","))
if q.active_variations:
vars_available.update(q.active_variations.split(","))
elif res[0] < Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_gone.update(q.active_items.split(","))
if q.active_variations:
vars_gone.update(q.active_variations.split(","))
if not self.active_quotas or (not quotas_for_item and not quotas_for_variation):
# No item is enabled for this event, treat the event as "unknown"
return None, None, None
items_available -= items_disabled
items_reserved -= items_disabled
items_gone -= items_disabled
vars_available -= vars_disabled
vars_reserved -= vars_disabled
vars_gone -= vars_disabled
# We iterate over all items and variations and keep track of
# - `best_state_found` - the best availability state we have seen so far. If one item is available, the event is available!
# - `num_tickets_found` - the number of tickets currently available in total. We sum up all the items and variations, but keep
# track of them per-quota in `quota_used_for_found_tickets` to make sure we don't count the same tickets twice if two or more
# items share the same quota
# - `num_tickets_possible` - basically the same thing, just with the total size of quotas instead of their currently availability
# since we need that for the percentage calculation
best_state_found = Quota.AVAILABILITY_GONE
num_tickets_found = 0
num_tickets_possible = 0
quota_used_for_found_tickets = Counter()
quota_used_for_possible_tickets = Counter()
for quota_list in list(quotas_for_item.values()) + list(quotas_for_variation.values()):
worst_state_for_ticket = min(r[q][0] for q in quota_list)
quotas_that_are_not_unlimited = [q for q in quota_list if q.size is not None]
if not quotas_that_are_not_unlimited:
# We found an unlimited ticket, no more need to do anything else
return Quota.AVAILABILITY_OK, None, None
if not self.active_quotas or (
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved
):
return None
if worst_state_for_ticket == Quota.AVAILABILITY_OK:
availability_of_this = min(max(0, r[q][1] - quota_used_for_found_tickets[q]) for q in quotas_that_are_not_unlimited)
num_tickets_found += availability_of_this
for q in quota_list:
quota_used_for_found_tickets[q] += availability_of_this
possible_of_this = min(max(0, q.size - quota_used_for_possible_tickets[q]) for q in quotas_that_are_not_unlimited)
num_tickets_possible += possible_of_this
for q in quota_list:
quota_used_for_possible_tickets[q] += possible_of_this
best_state_found = max(best_state_found, worst_state_for_ticket)
return best_state_found, num_tickets_found, num_tickets_possible
if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone:
return Quota.AVAILABILITY_OK
if items_reserved - items_gone or vars_reserved - vars_gone:
return Quota.AVAILABILITY_RESERVED
return Quota.AVAILABILITY_GONE
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
@@ -631,7 +591,6 @@ class Event(EventMixin, LoggedModel):
self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
@property
def social_image(self):
-3
View File
@@ -95,8 +95,6 @@ class Invoice(models.Model):
:type additional_text: str
:param payment_provider_text: A payment provider specific text
:type payment_provider_text: str
:param payment_provider_stamp: A payment provider specific stamp
:type payment_provider_stamp: str
:param footer_text: A footer text, displayed smaller and centered on every page
:type footer_text: str
:param foreign_currency_display: A different currency that taxes should also be displayed in.
@@ -146,7 +144,6 @@ class Invoice(models.Model):
additional_text = models.TextField(blank=True)
reverse_charge = models.BooleanField(default=False)
payment_provider_text = models.TextField(blank=True)
payment_provider_stamp = models.CharField(max_length=100, null=True, blank=True)
footer_text = models.TextField(blank=True)
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
+3 -5
View File
@@ -79,7 +79,6 @@ from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ._transactions import (
@@ -1629,13 +1628,13 @@ class OrderPayment(models.Model):
been marked as paid.
"""
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state not in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
# Race condition detected, this payment is already confirmed
logger.info('Failed payment {} but ignored due to likely race condition.'.format(
self.full_id,
))
return False
return
if isinstance(info, str):
locked_instance.info = info
@@ -1651,7 +1650,6 @@ class OrderPayment(models.Model):
'info': info,
'data': log_data,
}, user=user, auth=auth)
return True
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_date=None, generate_invoice=True):
@@ -1675,7 +1673,7 @@ class OrderPayment(models.Model):
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
# Race condition detected, this payment is already confirmed
logger.info('Confirmed payment {} but ignored due to likely race condition.'.format(
+1 -12
View File
@@ -67,7 +67,6 @@ from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map
from pretix.helpers.money import DecimalTextInput
@@ -464,16 +463,6 @@ class BasePaymentProvider:
return pgettext_lazy('invoice', 'The payment for this invoice has already been received.')
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
def render_invoice_stamp(self, order: Order, payment: OrderPayment) -> str:
"""
This is called when an invoice for an order with this payment provider is generated.
The default implementation returns "paid" if the order was already paid, and ``None``
otherwise. You can override this with a string, but it should be *really* short to make
the invoice look pretty.
"""
if order.status == Order.STATUS_PAID:
return _('paid')
@property
def payment_form_fields(self) -> dict:
"""
@@ -1410,7 +1399,7 @@ class GiftCardPayment(BasePaymentProvider):
try:
with transaction.atomic():
try:
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gcpk)
gc = GiftCard.objects.select_for_update().get(pk=gcpk)
except GiftCard.DoesNotExist:
raise PaymentException(_("This gift card does not support this currency."))
if gc.currency != self.event.currency: # noqa - just a safeguard
+9 -22
View File
@@ -62,7 +62,7 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString
from pypdf import PdfReader
from PyPDF2 import PdfReader
from pytz import timezone
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
@@ -848,17 +848,12 @@ class Renderer:
'center': TA_CENTER,
'right': TA_RIGHT
}
# lineheight display differs from browser canvas. This calc is just empirical values to get
# reportlab render similarly to browser canvas.
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
lineheight = float(o['lineheight']) * 1.15 if 'lineheight' in o else 1.0
style = ParagraphStyle(
name=uuid.uuid4().hex,
fontName=font,
fontSize=float(o['fontsize']),
leading=lineheight * float(o['fontsize']),
# for backwards compatability use autoLeading if no lineheight is given
autoLeading='off' if 'lineheight' in o else 'max',
leading=float(o['fontsize']),
autoLeading="max",
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']]
)
@@ -867,10 +862,6 @@ class Renderer:
self._get_text_content(op, order, o) or "",
).replace("\n", "<br/>\n").replace("-", "-&hairsp;")
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
@@ -878,6 +869,9 @@ class Renderer:
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
# reportlab does not support unicode combination characters
text = unicodedata.normalize("NFKC", text)
p = Paragraph(text, style=style)
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
@@ -887,15 +881,8 @@ class Renderer:
if o.get('downward', False):
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm)
canvas.rotate(o.get('rotation', 0) * -1)
p.drawOn(canvas, 0, -h - ad[1] / 2.5)
p.drawOn(canvas, 0, -h - ad[1] / 2)
else:
if lineheight != 1.0:
# lineheight adds to ascent/descent offsets, just empirical values again to get
# reportlab to render similarly to browser canvas
ad = (
ad[0],
ad[1] + (lineheight - 1.0) * float(o['fontsize']) * 1.05
)
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + h)
canvas.rotate(o.get('rotation', 0) * -1)
p.drawOn(canvas, 0, -h - ad[1])
@@ -950,8 +937,8 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from PyPDF2 import PdfReader, PdfWriter, Transformation
from PyPDF2.generic import RectangleObject
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter()
+1 -2
View File
@@ -41,7 +41,6 @@ from pretix.base.services.orders import (
)
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -240,7 +239,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
for o in orders_to_change.values_list('id', flat=True).iterator():
with transaction.atomic():
o = event.orders.select_for_update(of=OF_SELF).get(pk=o)
o = event.orders.select_for_update().get(pk=o)
total = Decimal('0.00')
fee = Decimal('0.00')
positions = []
+82 -121
View File
@@ -43,9 +43,7 @@ from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
from django.dispatch import receiver
from django.utils.timezone import make_aware, now
from django.utils.translation import (
gettext as _, gettext_lazy, ngettext_lazy, pgettext_lazy,
)
from django.utils.translation import gettext as _, pgettext_lazy
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
@@ -83,119 +81,74 @@ class CartError(Exception):
if msgargs:
msg = _(msg) % msgargs
else:
# force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
msg = _(msg)
super().__init__(msg)
error_messages = {
'busy': gettext_lazy(
'We were not able to process your request completely as the '
'server was too busy. Please try again.'
),
'empty': gettext_lazy('You did not select any products.'),
'unknown_position': gettext_lazy('Unknown cart position.'),
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'empty': _('You did not select any products.'),
'unknown_position': _('Unknown cart position.'),
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'unavailable': gettext_lazy(
'Some of the products you selected are no longer available. '
'Please see below for details.'
),
'in_part': gettext_lazy(
'Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'
),
'max_items': ngettext_lazy(
"You cannot select more than %s item per order.",
"You cannot select more than %s items per order."
),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s.",
"You cannot select more than %(max)s items of the product %(product)s.",
"max"
),
'min_items_per_product': ngettext_lazy(
"You need to select at least %(min)s item of the product %(product)s.",
"You need to select at least %(min)s items of the product %(product)s.",
"min"
),
'min_items_per_product_removed': ngettext_lazy(
"We removed %(product)s from your cart as you can not buy less than %(min)s item of it.",
"We removed %(product)s from your cart as you can not buy less than %(min)s items of it.",
"min"
),
'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'ended': gettext_lazy('The booking period for this event has ended.'),
'payment_ended': gettext_lazy('All payments for this event need to be confirmed already, so no new orders can be created.'),
'some_subevent_not_started': gettext_lazy(
'The booking period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'
),
'voucher_min_usages_removed': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'number'
),
'voucher_redeemed': gettext_lazy('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': gettext_lazy(
'This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
'to redeem it before but did not complete the checkout process. You can try to use it '
'again in %d minutes.'
),
'voucher_redeemed_partial': gettext_lazy('This voucher code can only be redeemed %d more times.'),
'voucher_whole_cart_not_combined': gettext_lazy('Applying a voucher to the whole cart should not be combined with other operations.'),
'voucher_double': gettext_lazy(
'You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'
),
'voucher_expired': gettext_lazy('This voucher is expired.'),
'voucher_invalid_item': gettext_lazy('This voucher is not valid for this product.'),
'voucher_invalid_seat': gettext_lazy('This voucher is not valid for this seat.'),
'voucher_no_match': gettext_lazy(
'We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'
),
'voucher_item_not_available': gettext_lazy(
'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. '
'Please see below for details.'),
'in_part': _('Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'),
'max_items': _("You cannot select more than %s items per order."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."),
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."),
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period for this event has ended.'),
'payment_ended': _('All payments for this event need to be confirmed already, so no new orders can be created.'),
'some_subevent_not_started': _('The booking period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_too_high': _('The entered price is to high.'),
'voucher_invalid': _('This voucher code is not known in our database.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_min_usages_removed': _('The voucher code "%(voucher)s" can only be used if you select at least '
'%(number)s matching products. We have therefore removed some positions from '
'your cart that can no longer be purchased like this.'),
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': _('This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
'to redeem it before but did not complete the checkout process. You can try to use it '
'again in %d minutes.'),
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
'voucher_no_match': _('We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'),
'voucher_item_not_available': _(
'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
'voucher_required': gettext_lazy('You need a valid voucher code to order this product.'),
'voucher_required': _('You need a valid voucher code to order this product.'),
'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'),
'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': ngettext_lazy(
'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'max'
),
'addon_min_count': ngettext_lazy(
'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'),
'seat_invalid': gettext_lazy('Please select a valid seat.'),
'seat_forbidden': gettext_lazy('You can not select a seat for this position.'),
'seat_unavailable': gettext_lazy('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': gettext_lazy('You can not select the same seat multiple times.'),
'gift_card': gettext_lazy("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': _('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
'seat_required': _('You need to select a specific seat.'),
'seat_invalid': _('Please select a valid seat.'),
'seat_forbidden': _('You can not select a seat for this position.'),
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': _('You can not select the same seat multiple times.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': _('One of the selected products is not available in the selected country.'),
}
@@ -367,7 +320,8 @@ class CartManager:
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order):
raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
# TODO: i18n plurals
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -540,7 +494,7 @@ class CartManager:
def apply_voucher(self, voucher_code: str):
if self._operations:
raise CartError(error_messages['voucher_whole_cart_not_combined'])
raise CartError('Applying a voucher to the whole cart should not be combined with other operations.')
try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist:
@@ -585,7 +539,7 @@ class CartManager:
for voucher, cnt in list(voucher_use_diff.items()):
if 0 < cnt < voucher.min_usages_remaining:
raise CartError(
error_messages['voucher_min_usages'] % {
_(error_messages['voucher_min_usages']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
@@ -889,16 +843,22 @@ class CartManager:
for (i, v), c in selected.items():
n_per_i[i] += c
if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError(
error_messages['addon_max_count'] % {
error_messages['addon_max_count'],
{
'base': str(item.name),
'max': iao.max_count,
'cat': str(iao.addon_category.name),
}
)
elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError(
error_messages['addon_min_count'] % {
error_messages['addon_min_count'],
{
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
@@ -906,7 +866,8 @@ class CartManager:
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise CartError(
error_messages['addon_no_multi'] % {
error_messages['addon_no_multi'],
{
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
@@ -970,7 +931,7 @@ class CartManager:
if item.max_per_order and count > item.max_per_order:
raise CartError(
error_messages['max_items_per_product'] % {
_(error_messages['max_items_per_product']) % {
'max': item.max_per_order,
'product': item.name
}
@@ -984,13 +945,13 @@ class CartManager:
for p in self.positions:
if p.item_id == item.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = error_messages['min_items_per_product_removed'] % {
err = _(error_messages['min_items_per_product_removed']) % {
'min': item.min_per_order,
'product': item.name
}
if not err:
raise CartError(
error_messages['min_items_per_product'] % {
_(error_messages['min_items_per_product']) % {
'min': item.min_per_order,
'product': item.name
}
@@ -1019,13 +980,13 @@ class CartManager:
for p in self.positions:
if p.voucher_id == voucher.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = error_messages['voucher_min_usages_removed'] % {
err = _(error_messages['voucher_min_usages_removed']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
if not err:
raise CartError(
error_messages['voucher_min_usages'] % {
_(error_messages['voucher_min_usages']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
@@ -1073,11 +1034,11 @@ class CartManager:
if voucher_available_count < 1:
if op.voucher in self._voucher_depend_on_cart:
err = err or (error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time)
err = err or _(error_messages['voucher_redeemed_cart']) % self.event.settings.reservation_time
else:
err = err or error_messages['voucher_redeemed']
elif voucher_available_count < requested_count:
err = err or (error_messages['voucher_redeemed_partial'] % voucher_available_count)
err = err or _(error_messages['voucher_redeemed_partial']) % voucher_available_count
available_count = min(quota_available_count, voucher_available_count)
+6 -12
View File
@@ -56,7 +56,6 @@ from pretix.base.models import (
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
)
from pretix.base.signals import checkin_created, order_placed, periodic_task
from pretix.helpers import OF_SELF
from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
from pretix.helpers.jsonlogic_query import (
@@ -290,11 +289,6 @@ def _logic_explain(rules, ev, rule_data):
p for i, p in enumerate(paths) if path_weights[i] == min_weight
]
# Step 7: All things equal, prefer shorter explanations
paths_with_min_weight.sort(
key=lambda p: len([v for v in p if not _var_values[v]])
)
# Finally, return the text for one of them
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
@@ -735,11 +729,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
_save_answers(op, answers, given_answers)
with transaction.atomic():
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all
if type != Checkin.TYPE_EXIT:
opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk)
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
raise CheckInError(
@@ -851,7 +842,10 @@ def process_exit_all(sender, **kwargs):
exit_all_at__isnull=False
).select_related('event', 'event__organizer')
for cl in qs:
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
positions = cl.positions_inside.filter(
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
last_entry__lte=cl.exit_all_at,
)
for p in positions:
with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create(
+2 -3
View File
@@ -49,7 +49,6 @@ from pretix.base.signals import (
periodic_task, register_data_exporters, register_multievent_data_exporters,
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -345,7 +344,7 @@ def run_scheduled_exports(sender, **kwargs):
qs = ScheduledEventExport.objects.filter(
schedule_next_run__lt=now(),
error_counter__lt=5,
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked, of=OF_SELF).select_related('event')
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked).select_related('event')
for s in qs:
scheduled_event_export.apply_async(kwargs={
'event': s.event_id,
@@ -356,7 +355,7 @@ def run_scheduled_exports(sender, **kwargs):
qs = ScheduledOrganizerExport.objects.filter(
schedule_next_run__lt=now(),
error_counter__lt=5,
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked, of=OF_SELF).select_related('organizer')
).select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked).select_related('organizer')
for s in qs:
scheduled_organizer_export.apply_async(kwargs={
'organizer': s.organizer_id,
+2 -7
View File
@@ -63,7 +63,7 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import invoice_line_text, periodic_task
from pretix.celery_app import app
from pretix.helpers.database import OF_SELF, rolledback_transaction
from pretix.helpers.database import rolledback_transaction
from pretix.helpers.models import modelcopy
logger = logging.getLogger(__name__)
@@ -98,10 +98,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
payment = str(lp.payment_provider.render_invoice_text(invoice.order))
payment_stamp = lp.payment_provider.render_invoice_stamp(invoice.order, lp)
else:
payment = ""
payment_stamp = None
if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
if payment:
payment += "<br /><br />"
@@ -113,7 +111,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None
try:
ia = invoice.order.invoice_address
@@ -328,7 +325,6 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.is_cancellation = True
cancellation.date = timezone.now().date()
cancellation.payment_provider_text = ''
cancellation.payment_provider_stamp = ''
cancellation.file = None
cancellation.sent_to_organizer = None
cancellation.sent_to_customer = None
@@ -440,7 +436,6 @@ def build_preview_invoice_pdf(event):
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = _('paid')
invoice.invoice_to_name = _("John Doe")
invoice.invoice_to_street = _("214th Example Street")
invoice.invoice_to_zipcode = _("012345")
@@ -505,7 +500,7 @@ def send_invoices_to_organizer(sender, **kwargs):
with transaction.atomic():
qs = Invoice.objects.filter(
sent_to_organizer__isnull=True
).prefetch_related('event').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
).prefetch_related('event').select_for_update(skip_locked=connection.features.has_select_for_update_skip_locked)
for i in qs[:batch_size]:
if i.event.settings.invoice_email_organizer:
with language(i.event.settings.locale):
+1 -2
View File
@@ -32,7 +32,6 @@ from pretix.base.models import (
AbstractPosition, Customer, Event, Item, Membership, Order, OrderPosition,
SubEvent,
)
from pretix.helpers import OF_SELF
def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
@@ -119,7 +118,7 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
base_qs = Membership.objects.with_usages(ignored_order=ignored_order)
if lock:
base_qs = base_qs.select_for_update(of=OF_SELF)
base_qs = base_qs.select_for_update()
membership_cache = base_qs\
.select_related('membership_type')\
+103 -130
View File
@@ -54,13 +54,15 @@ from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.i18n import (
LazyLocaleException, get_language_without_region, language,
)
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -95,98 +97,50 @@ from pretix.base.signals import (
order_placed, order_split, periodic_task, validate_order,
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy
from pretix.helpers.periodic import minimum_interval
class OrderError(Exception):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
# force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
super().__init__(msg)
error_messages = {
'unavailable': gettext_lazy(
'Some of the products you selected were no longer available. '
'Please see below for details.'
),
'in_part': gettext_lazy(
'Some of the products you selected were no longer available in '
'the quantity you selected. Please see below for details.'
),
'price_changed': gettext_lazy(
'The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'
),
'internal': gettext_lazy("An internal error occurred, please try again."),
'empty': gettext_lazy("Your cart is empty."),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s. We removed the surplus items from your cart.",
"You cannot select more than %(max)s items of the product %(product)s. We removed the surplus items from your cart.",
"max"
),
'busy': gettext_lazy(
'We were not able to process your request completely as the '
'server was too busy. Please try again.'
),
'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'ended': gettext_lazy('The booking period has ended.'),
'voucher_min_usages': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'number'
),
'voucher_invalid': gettext_lazy('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': gettext_lazy(
'The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'
),
'voucher_budget_used': gettext_lazy(
'The voucher code used for one of the items in your cart has already been too often. We '
'adjusted the price of the item in your cart.'
),
'voucher_expired': gettext_lazy(
'The voucher code used for one of the items in your cart is expired. We removed this item from your cart.'
),
'voucher_invalid_item': gettext_lazy(
'The voucher code used for one of the items in your cart is not valid for this item. We removed this item from your cart.'
),
'voucher_required': gettext_lazy('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': gettext_lazy(
'The booking period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'
),
'some_subevent_ended': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'
),
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': ngettext_lazy(
'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'max'
),
'addon_min_count': ngettext_lazy(
'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'unavailable': _('Some of the products you selected were no longer available. '
'Please see below for details.'),
'in_part': _('Some of the products you selected were no longer available in '
'the quantity you selected. Please see below for details.'),
'price_changed': _('The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'),
'internal': _("An internal error occurred, please try again."),
'empty': _("Your cart is empty."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s. We removed the "
"surplus items from your cart."),
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period has ended.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'),
'voucher_budget_used': _('The voucher code used for one of the items in your cart has already been too often. We '
'adjusted the price of the item in your cart.'),
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
'from your cart.'),
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
'removed this item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': _('The booking period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'),
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': _('One of the selected products is not available in the selected country.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
}
logger = logging.getLogger(__name__)
@@ -202,7 +156,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
enough quota.
"""
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:
is_available = order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True,
@@ -230,7 +184,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
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().get(pk=gc.pk)
gc.transactions.create(value=position.price, order=order)
break
@@ -443,7 +397,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
# If new actions are added to this function, make sure to add the reverse operation to reactivate_order()
with transaction.atomic():
if isinstance(order, int):
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order)
order = Order.objects.select_for_update().get(pk=order)
if isinstance(user, int):
user = User.objects.get(pk=user)
if isinstance(api_token, int):
@@ -465,7 +419,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
for position in order.positions.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().get(pk=gc.pk)
if gc.value < position.price:
raise OrderError(
_('This order can not be canceled since the gift card {card} purchased in '
@@ -583,6 +537,18 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
return order.pk
class OrderError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def _check_date(event: Event, now_dt: datetime):
if event.presale_start and now_dt < event.presale_start:
raise OrderError(error_messages['not_started'])
@@ -603,6 +569,7 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
sales_channel='web', customer=None):
err = None
errargs = None
_check_date(event, now_dt)
products_seen = Counter()
@@ -639,10 +606,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen[cp.item] += 1
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
err = error_messages['max_items_per_product'] % {
'max': cp.item.max_per_order,
'product': cp.item.name
}
err = error_messages['max_items_per_product']
errargs = {'max': cp.item.max_per_order,
'product': cp.item.name}
delete(cp)
break
@@ -761,7 +727,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
for voucher, cnt in v_usages.items():
if 0 < cnt < voucher.min_usages_remaining:
raise OrderError(error_messages['voucher_min_usages'] % {
raise OrderError(error_messages['voucher_min_usages'], {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
})
@@ -824,7 +790,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.save()
if err:
raise OrderError(err)
raise OrderError(err, errargs)
def _get_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
@@ -1239,7 +1205,7 @@ def send_expiry_warnings(sender, **kwargs):
if days and (o.expires - today).days <= days:
with transaction.atomic():
o = Order.objects.select_related('event').select_for_update(of=OF_SELF).get(pk=o.pk)
o = Order.objects.select_related('event').select_for_update().get(pk=o.pk)
if o.status != Order.STATUS_PENDING or o.expiry_reminder_sent:
# Race condition
continue
@@ -1298,7 +1264,7 @@ def send_download_reminders(sender, **kwargs):
continue
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
o = Order.objects.select_for_update().get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
@@ -1377,26 +1343,23 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
class OrderChangeManager:
error_messages = {
'product_without_variation': gettext_lazy('You need to select a variation of the product.'),
'quota': gettext_lazy('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': gettext_lazy('There is no quota defined that allows this operation.'),
'product_invalid': gettext_lazy('The selected product is not active or has no price set.'),
'complete_cancel': gettext_lazy('This operation would leave the order empty. Please cancel the order itself instead.'),
'paid_to_free_exceeded': gettext_lazy(
'This operation would make the order free and therefore immediately paid, however '
'no quota is available.'
),
'addon_to_required': gettext_lazy('This is an add-on product, please select the base position it should be added to.'),
'addon_invalid': gettext_lazy('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': gettext_lazy('You need to choose a subevent for the new position.'),
'seat_unavailable': gettext_lazy('The selected seat "{seat}" is not available.'),
'seat_subevent_mismatch': gettext_lazy(
'You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'
),
'seat_required': gettext_lazy('The selected product requires you to select a seat.'),
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
'order_canceled': _('You cannot change a canceled order.'),
'product_without_variation': _('You need to select a variation of the product.'),
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': _('There is no quota defined that allows this operation.'),
'product_invalid': _('The selected product is not active or has no price set.'),
'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'),
'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however '
'no quota is available.'),
'addon_to_required': _('This is an add-on product, please select the base position it should be added to.'),
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': _('You need to choose a subevent for the new position.'),
'seat_unavailable': _('The selected seat "{seat}" is not available.'),
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'),
'seat_required': _('The selected product requires you to select a seat.'),
'seat_forbidden': _('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': _('The selected country is blocked by your tax rule.'),
'gift_card_change': _('You cannot change the price of a position that has been used to issue a gift card.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
@@ -1428,6 +1391,9 @@ class OrderChangeManager:
self._invoice_dirty = False
self._invoices = []
if order.status == Order.STATUS_CANCELED:
raise OrderError(self.error_messages['order_canceled'])
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
@@ -1803,16 +1769,22 @@ class OrderChangeManager:
for (i, v), c in selected.items():
n_per_i[i] += c
if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_max_count'] % {
error_messages['addon_max_count'],
{
'base': str(item.name),
'max': iao.max_count,
'cat': str(iao.addon_category.name),
}
)
elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_min_count'] % {
error_messages['addon_min_count'],
{
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
@@ -1820,7 +1792,8 @@ class OrderChangeManager:
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise OrderError(
error_messages['addon_no_multi'] % {
error_messages['addon_no_multi'],
{
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
@@ -2090,7 +2063,7 @@ class OrderChangeManager:
op.fee.save(update_fields=['canceled'])
elif isinstance(op, self.CancelOperation):
for gc in op.position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
if gc.value < op.position.price:
raise OrderError(_(
'A position can not be canceled since the gift card {card} purchased in this order has '
@@ -2106,7 +2079,7 @@ class OrderChangeManager:
for opa in op.position.addons.all():
for gc in opa.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
if gc.value < opa.position.price:
raise OrderError(_(
'A position can not be canceled since the gift card {card} purchased in this order has '
@@ -2501,7 +2474,7 @@ def perform_order(self, event: Event, payments: List[dict], positions: List[str]
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise OrderError(error_messages['busy'])
raise OrderError(str(error_messages['busy']))
_unset = object()
@@ -2679,9 +2652,9 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
open_payment = None
if new_payment:
lp = order.payments.select_for_update(of=OF_SELF).exclude(pk=new_payment.pk).last()
lp = order.payments.select_for_update().exclude(pk=new_payment.pk).last()
else:
lp = order.payments.select_for_update(of=OF_SELF).last()
lp = order.payments.select_for_update().last()
if lp and lp.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
open_payment = lp
-19
View File
@@ -1308,25 +1308,6 @@ DEFAULTS = {
"the email. Does not affect orders performed through other sales channels."),
)
},
'low_availability_percentage': {
'default': None,
'type': int,
'serializer_class': serializers.IntegerField,
'form_class': forms.IntegerField,
'serializer_kwargs': dict(
min_value=0,
max_value=100,
),
'form_kwargs': dict(
label=_('Low availability threshold'),
help_text=_('If the availability of tickets falls below this percentage, the event (or a date, if it is an '
'event series) will be highlighted to have low availability in the event list or calendar. If '
'you keep this option empty, low availability will not be shown publicly.'),
min_value=0,
max_value=100,
required=False
)
},
'event_list_availability': {
'default': 'True',
'type': bool,
+2 -4
View File
@@ -293,16 +293,14 @@ class DateFrameWidget(forms.MultiWidget):
if '/' in value:
return [
'custom',
date.fromisoformat(value.split('/', 1)[0]) if value.split('/', 1)[0] else None,
date.fromisoformat(value.split('/', 1)[-1]) if value.split('/', 1)[-1] else None,
date.fromisoformat(value.split('/', 1)[0]),
date.fromisoformat(value.split('/', 1)[-1]),
]
return [value, None, None]
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['required'] = self.timeframe_choices[0][0] == 'unset'
ctx['widget']['subwidgets'][1]['attrs'].pop('required', None)
ctx['widget']['subwidgets'][2]['attrs'].pop('required', None)
return ctx
-1
View File
@@ -507,7 +507,6 @@ class EventSettingsForm(SettingsForm):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'low_availability_percentage',
'event_list_type',
'event_list_available_only',
'frontpage_text',
+5 -48
View File
@@ -57,8 +57,8 @@ from pretix.base.forms.widgets import (
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent, Team,
TeamAPIToken, TeamInvite,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2
@@ -1116,25 +1116,9 @@ class SubEventFilterForm(FilterForm):
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['date_from'].widget = DatePickerWidget()
self.fields['date_until'].widget = DatePickerWidget()
for p in self.meta_properties.all():
self.fields['meta_{}'.format(p.name)] = forms.CharField(
label=p.name,
required=False,
widget=forms.TextInput(
attrs={
'data-typeahead-url': reverse('control:event.subevents.meta.typeahead', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug
}) + '?' + urlencode({
'property': p.name,
})
}
)
)
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -1197,31 +1181,6 @@ class SubEventFilterForm(FilterForm):
if fdata.get('time_from'):
qs = qs.filter(date_from__time__gte=fdata.get('time_from'))
filters_by_property_name = {}
for i, p in enumerate(self.meta_properties):
d = fdata.get('meta_{}'.format(p.name))
if d:
semv_with_value = SubEventMetaValue.objects.filter(
subevent=OuterRef('pk'),
property__pk=p.pk,
value=d
)
semv_with_any_value = SubEventMetaValue.objects.filter(
subevent=OuterRef('pk'),
property__pk=p.pk,
)
qs = qs.annotate(**{'attr_{}'.format(i): Exists(semv_with_value)})
if p.name in filters_by_property_name:
filters_by_property_name[p.name] |= Q(**{'attr_{}'.format(i): True})
else:
filters_by_property_name[p.name] = Q(**{'attr_{}'.format(i): True})
default = self.event.meta_data[p.name]
if default == d:
qs = qs.annotate(**{'attr_{}_any'.format(i): Exists(semv_with_any_value)})
filters_by_property_name[p.name] |= Q(**{'attr_{}_any'.format(i): False})
for f in filters_by_property_name.values():
qs = qs.filter(f)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
@@ -1229,10 +1188,6 @@ class SubEventFilterForm(FilterForm):
return qs
@cached_property
def meta_properties(self):
return self.event.organizer.meta_properties.filter(filter_allowed=True)
class OrganizerFilterForm(FilterForm):
orders = {
@@ -1748,7 +1703,9 @@ class CheckinListAttendeeFilterForm(FilterForm):
if s == '1':
qs = qs.filter(last_entry__isnull=False)
elif s == '2':
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
qs = qs.filter(last_entry__isnull=False).filter(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
elif s == '3':
qs = qs.filter(last_entry__isnull=False).filter(
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
+1 -1
View File
@@ -564,7 +564,7 @@ class WebHookForm(forms.ModelForm):
class Meta:
model = WebHook
fields = ['target_url', 'enabled', 'all_events', 'limit_events', 'comment']
fields = ['target_url', 'enabled', 'all_events', 'limit_events']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
-15
View File
@@ -23,7 +23,6 @@ from datetime import datetime
from urllib.parse import urlencode
from django import forms
from django.core.exceptions import ValidationError
from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
@@ -274,13 +273,6 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
'available_until': SplitDateTimeField,
}
def clean(self):
d = super().clean()
if d.get('available_from') and d.get('available_until'):
if d.get('available_from') > d.get('available_until'):
raise ValidationError(_('The end of availability should be after the start of availability.'))
return d
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
@@ -301,13 +293,6 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
'available_until': SplitDateTimeField,
}
def clean(self):
d = super().clean()
if d.get('available_from') and d.get('available_until'):
if d.get('available_from') > d.get('available_until'):
raise ValidationError(_('The end of availability should be after the start of availability.'))
return d
class BulkSubEventItemForm(SubEventItemForm):
rel_available_from = RelativeDateTimeField(
@@ -423,17 +423,6 @@
</div>
{% endif %}
{% if "mysql" in settings.DATABASES.default.ENGINE and not request.organizer %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
You are using MySQL or MariaDB as your database backend for pretix.
Starting in pretix 5.0, these will no longer be supported and you will need to migrate to PostgreSQL.
Please see the pretix administrator documentation for a migration guide, and the pretix 4.16
release notes for more information.
{% endblocktrans %}
</div>
{% endif %}
{% if debug_warning %}
<div class="alert alert-danger">
{% trans "pretix is running in debug mode. For security reasons, please never run debug mode on a production instance." %}
@@ -24,7 +24,7 @@
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.cardName|default_if_none:payment_info.payment_data.cardType }}</dd>
<dd>{{ payment_info.payment_data.cardName }}</dd>
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.expiry }}</dd>
{% elif payment_info.payment_type == "sumup" %}
@@ -98,8 +98,5 @@
<dd>{{ payment_info.payment_data.posEntryMode }}</dd>
<dt>{% trans "Result Code" %}</dt>
<dd>{{ payment_info.payment_data.posResultCode }}</dd>
{% elif payment_info.payment_type == "cash" %}
<dt>{% trans "Payment method" %}</dt>
<dd>{% trans "Cash" %}</dd>
{% endif %}
</dl>
@@ -314,8 +314,7 @@
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
@@ -11,7 +11,6 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.target_url layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.enabled layout="control" %}
{% bootstrap_field form.events layout="control" %}
{% bootstrap_field form.all_events layout="control" %}
@@ -90,7 +90,7 @@
</div>
</details>
{% empty %}
<div class="alert alert-info">{% trans "This webhook did not receive any events in the last 30 days." %}</div>
<div class="alert-info">{% trans "This webhook did not receive any events in the last 30 days." %}</div>
{% endfor %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}
@@ -35,7 +35,6 @@
<tr>
<th>{% trans "Target URL" %}</th>
<th>{% trans "Events" %}</th>
<th>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
@@ -62,11 +61,6 @@
</ul>
{% endif %}
</td>
<td>
{% if w.comment %}
{{ w.comment }}
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.webhook.edit" organizer=request.organizer.slug webhook=w.id %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Edit" %}">
@@ -275,18 +275,6 @@
<input type="number" value="13" class="input-block-level form-control" step="0.1"
id="toolbox-fontsize">
</div>
<div class="col-sm-6">
<label>{% trans "Line height" %}</label><br>
<input type="number" value="1" class="input-block-level form-control" step="0.1"
id="toolbox-lineheight">
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" role="group">
@@ -307,6 +295,15 @@
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" id="toolbox-align">
<div class="btn-group" role="group">
@@ -487,7 +487,6 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -151,7 +151,6 @@
<fieldset>
<legend>{% trans "Item prices" %}</legend>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -129,7 +129,6 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -50,11 +50,6 @@
<div class="col-xs-12 one-line-checkboxes">
{% bootstrap_field filter_form.weekday %}
</div>
{% for mf in meta_fields %}
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field mf %}
</div>
{% endfor %}
</div>
<div class="text-right flip">
-1
View File
@@ -261,7 +261,6 @@ urlpatterns = [
re_path(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'),
re_path(r'^subevents/bulk_action$', subevents.SubEventBulkAction.as_view(), name='event.subevents.bulkaction'),
re_path(r'^subevents/bulk_edit$', subevents.SubEventBulkEdit.as_view(), name='event.subevents.bulkedit'),
re_path(r'^subevents/typeahead/meta/$', typeahead.subevent_meta_values, name='event.subevents.meta.typeahead'),
re_path(r'^items/$', item.ItemList.as_view(), name='event.items'),
re_path(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
+2 -7
View File
@@ -92,12 +92,7 @@ class CheckInListQueryMixin:
last_entry=Subquery(cqs),
last_exit=Subquery(cqs_exit),
auto_checked_in=Exists(
Checkin.objects.filter(
position_id=OuterRef('pk'),
type=Checkin.TYPE_ENTRY,
list_id=self.list.pk,
auto_checked_in=True
)
Checkin.objects.filter(position_id=OuterRef('pk'), list_id=self.list.pk, auto_checked_in=True)
)
).select_related(
'item', 'variation', 'order', 'addon_to'
@@ -252,7 +247,7 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'list': self.list.pk
}) + ('?' + value[1] if value and value[1] else '')
}) + ('?' + value[1] if value[1] else '')
class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
+3 -22
View File
@@ -42,7 +42,6 @@ from itertools import groupby
from urllib.parse import urlsplit
import bleach
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -1026,27 +1025,9 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
self.request.event.delete()
messages.success(self.request, _('The event has been deleted.'))
return redirect(self.get_success_url())
except ProtectedError as e:
err = gettext('The event could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
app_labels = set()
for e in e.protected_objects:
app_labels.add(type(e)._meta.app_label)
plugin_names = []
for app_label in app_labels:
app = apps.get_app_config(app_label)
if hasattr(app, 'PretixPluginMeta'):
plugin_names.append(str(app.PretixPluginMeta.name))
else:
plugin_names.append(str(app.verbose_name))
if plugin_names:
err += ' ' + gettext(
'Specifically, the following plugins still contain data depends on this event: {plugin_names}'
).format(plugin_names=', '.join(plugin_names))
messages.error(self.request, err)
except ProtectedError:
messages.error(self.request, _('The event could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'))
return self.get(self.request, *self.args, **self.kwargs)
def get_success_url(self) -> str:
+1 -3
View File
@@ -280,9 +280,7 @@ class EventWizard(SafeSessionWizardView):
elif event.organizer.settings.event_team_provisioning:
t = Team.objects.create(
organizer=event.organizer,
name=_('Team {event}').format(
event=str(event.name)[:100] + "" if len(str(event.name)) > 100 else str(event.name)
),
name=_('Team {event}').format(event=str(event.name)[:188] + "" if len(str(event.name)) > 190 else str(event.name)),
can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
can_change_vouchers=True
+5 -19
View File
@@ -110,7 +110,7 @@ from pretix.control.permissions import (
from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers import GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import format_map
@@ -393,23 +393,9 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
self.request.organizer.delete()
messages.success(self.request, _('The organizer has been deleted.'))
return redirect(self.get_success_url())
except ProtectedError as e:
err = gettext('The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
# Unlike deleting events (which is done by regular backend users), this feature can only be used by sysadmins,
# so we expose more technical / less polished information.
affected_models = set()
for m in e.protected_objects:
affected_models.add(type(m)._meta.label)
if affected_models:
err += ' ' + gettext(
'The following database models still contain data that cannot be deleted automatically: {affected_models}'
).format(
affected_models=', '.join(list(affected_models))
)
messages.error(self.request, err)
except ProtectedError:
messages.error(self.request, _('The organizer could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'))
return self.get(self.request, *self.args, **self.kwargs)
def get_success_url(self) -> str:
@@ -1395,7 +1381,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
@transaction.atomic()
def post(self, request, *args, **kwargs):
self.object = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
self.object = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
if 'revert' in request.POST:
t = get_object_or_404(self.object.transactions.all(), pk=request.POST.get('revert'), order__isnull=False)
if self.object.value - t.value < Decimal('0.00'):
+2 -2
View File
@@ -39,8 +39,8 @@ from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from pypdf import PdfReader, PdfWriter
from pypdf.errors import PdfReadError
from PyPDF2 import PdfReader, PdfWriter
from PyPDF2.errors import PdfReadError
from reportlab.lib.units import mm
from pretix.base.i18n import language
+1 -4
View File
@@ -110,7 +110,7 @@ class SubEventQueryMixin:
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
return SubEventFilterForm(data=self.request_data, prefix='filter')
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryMixin, ListView):
@@ -125,9 +125,6 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['meta_fields'] = [
self.filter_form['meta_{}'.format(p.name)] for p in self.request.organizer.meta_properties.filter(filter_allowed=True)
]
quotas = []
for s in ctx['subevents']:
+2 -35
View File
@@ -48,8 +48,7 @@ from django.utils.translation import gettext as _, pgettext
from pretix.base.models import (
EventMetaProperty, EventMetaValue, ItemMetaProperty, ItemMetaValue,
ItemVariation, ItemVariationMetaValue, Order, Organizer, SubEventMetaValue,
User, Voucher,
ItemVariation, ItemVariationMetaValue, Order, Organizer, User, Voucher,
)
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import (
@@ -557,7 +556,7 @@ def itemvarquota_select2(request, **kwargs):
if not request.event.has_subevents:
# We are very unlikely to need pagination
itemqs = request.event.items.prefetch_related('variations').filter(Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query))
itemqs = request.event.items.prefetch_related('variations').filter(name__icontains=i18ncomp(query))
quotaqs = request.event.quotas.filter(name__icontains=query)
more = False
else:
@@ -744,38 +743,6 @@ def meta_values(request):
})
def subevent_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
matches = SubEventMetaValue.objects.filter(
value__icontains=q,
property__name=propname,
subevent__event_id=request.event.pk,
)
event_matches = EventMetaValue.objects.filter(
value__icontains=q,
property__name=propname,
event_id=request.event.pk,
)
defaults = EventMetaProperty.objects.filter(
default__icontains=q,
name=propname,
organizer_id=request.organizer.pk,
)
return JsonResponse({
'results': [
{'name': v, 'id': v}
for v in sorted(
set(defaults.values_list('default', flat=True)[:10]) |
set(matches.values_list('value', flat=True)[:10]) |
set(event_matches.values_list('value', flat=True)[:10])
)
]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
+1 -7
View File
@@ -21,9 +21,8 @@
#
import contextlib
from django.db import connection, transaction
from django.db import transaction
from django.db.models import Aggregate, Expression, Field, Lookup, Value
from django.utils.functional import lazy
class DummyRollbackException(Exception):
@@ -143,8 +142,3 @@ class PostgresWindowFrame(Expression):
"start": self.start.value,
"end": self.end.value,
}
# This is a short-hand for .select_for_update(of=("self,")), that falls back gracefully on databases that don't support
# the SELECT FOR UPDATE OF ... query.
OF_SELF = lazy(lambda: ("self",) if connection.features.has_select_for_update_of else (), tuple)()
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -543,45 +543,45 @@ msgstr "توليد الرسائل …"
msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -526,45 +526,45 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2023-01-13 08:00+0000\n"
"Last-Translator: tree <jan.stransky@arnal.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -540,16 +540,16 @@ msgstr "Vytváření zpráv…"
msgid "Unknown error."
msgstr "Neznámá chyba."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "Tato barva má velmi dobrý kontrast a je velmi dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Tato barva má slušný kontrast a pravděpodobně je dostatečně dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -557,31 +557,31 @@ msgstr ""
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
"tmavší odstín."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -561,45 +561,45 @@ msgstr "Opretter beskeder …"
msgid "Unknown error."
msgstr "Ukendt fejl."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -543,17 +543,17 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und ist wahrscheinlich gut zu "
"lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -561,31 +561,31 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!"
-3
View File
@@ -48,7 +48,6 @@ BezahlCode
Bezahlmethode
Blackberry
BLIK
Blogpost
BN
Branding
Browsereinstellungen
@@ -166,7 +165,6 @@ Linktext
loszulegen
Ltd
max
MariaDB
MapQuest
Mercado
Merchandise
@@ -208,7 +206,6 @@ Plugin
Plugins
POS
Postfix
PostgreSQL
prefix
pretix
pretixdesk
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -542,17 +542,17 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und ist wahrscheinlich gut zu "
"lesen!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -560,31 +560,31 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wähle eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!"
@@ -48,7 +48,6 @@ BezahlCode
Bezahlmethode
Blackberry
BLIK
Blogpost
BN
Branding
Browsereinstellungen
@@ -166,7 +165,6 @@ Linktext
loszulegen
Ltd
max
MariaDB
MapQuest
Mercado
Merchandise
@@ -208,7 +206,6 @@ Plugin
Plugins
POS
Postfix
PostgreSQL
prefix
pretix
pretixdesk
+1646 -1751
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+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"
@@ -525,45 +525,45 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -580,18 +580,18 @@ msgstr "Δημιουργία μηνυμάτων …"
msgid "Unknown error."
msgstr "Άγνωστο σφάλμα."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
"Το χρώμα σας έχει μεγάλη αντίθεση και είναι πολύ εύκολο να το διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Το χρώμα σας έχει αξιοπρεπή αντίθεση και είναι ίσως αρκετά καλό για να "
"διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -599,31 +599,31 @@ msgstr ""
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
"σκούρα σκιά."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Όλα"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -543,17 +543,17 @@ msgstr "Generando mensajes…"
msgid "Unknown error."
msgstr "Error desconocido."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "¡Tu color tiene gran contraste y es muy fácil de leer!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"¡Tu color tiene un contraste decente y es probablemente lo suficientemente "
"legible!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -561,31 +561,31 @@ msgstr ""
"Tu color tiene mal contraste para un texto con fondo blanco, por favor, "
"escoge un tono más oscuro."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Consultar búsqueda"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Solamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "¡Tienes cambios sin guardar!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -546,45 +546,45 @@ msgstr ""
msgid "Unknown error."
msgstr "Tuntematon virhe."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Kaikki"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Käytä toista nimeä sisäisesti"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Sulje klikkaamalla"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Sinulla on tallentamattomia muutoksia!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2023-01-17 01:00+0000\n"
"Last-Translator: Maurice Kaag <maurice@kaag.me>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -569,16 +569,16 @@ msgstr "Création de messages …"
msgid "Unknown error."
msgstr "Erreur inconnue."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "Votre choix couleur a un bon contraste et il est très facile à lire!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Votre choix de couleur est assez bon pour la lecture et a un bon contraste !"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -586,31 +586,31 @@ msgstr ""
"Votre choix de couleur n'a pas un bon contraste avec du texte sur un fond "
"blanc, SVP choisissez un ton plus sombre."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Tous"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Aucun"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Utiliser un nom différent en interne"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Cliquez pour fermer"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Vous avez des modifications non sauvegardées !"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -541,17 +541,17 @@ msgstr "Xerando mensaxes…"
msgid "Unknown error."
msgstr "Erro descoñecido."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "A túa cor ten moito contraste e é moi doada de ler!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"A túa cor ten un contraste axeitado e probablemente sexa suficientemente "
"lexible!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -559,31 +559,31 @@ msgstr ""
"A túa cor ten mal contraste para un texto con fondo branco. Por favor, "
"escolle un ton máis escuro."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Ningún"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Consultar unha procura"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Soamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Usar un nome diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Tes cambios sen gardar!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -529,45 +529,45 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -525,45 +525,45 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -570,16 +570,16 @@ msgstr "Üzenetek generálása…"
msgid "Unknown error."
msgstr "Ismeretlen hiba."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "A választott színek remek kontrasztot adnak, és nagyon könnyű olvasni!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"A választott színek kontrasztja elégséges, és valószínűleg jól olvasható!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -587,31 +587,31 @@ msgstr ""
"A választott színek kontrasztja elégtelen, kérjük válassz sötétebb "
"árnyalatot."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Összes"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Semmi"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Használj másik nevet"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Bezárásért kattints"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Mentetlen változtatások!"
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"POT-Creation-Date: 2023-01-19 10:47+0000\n"
"PO-Revision-Date: 2022-05-08 19:00+0000\n"
"Last-Translator: Emanuele Signoretta <signorettae@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -542,47 +542,47 @@ msgstr "Stiamo generando i messaggi …"
msgid "Unknown error."
msgstr "Errore sconosciuto."
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:308
msgid "Your color has great contrast and is very easy to read!"
msgstr "Il colore scelto ha un ottimo contrasto ed è molto leggibile!"
#: pretix/static/pretixcontrol/js/ui/main.js:311
#: pretix/static/pretixcontrol/js/ui/main.js:312
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Il colore scelto ha un buon contrasto e probabilmente è abbastanza leggibile!"
#: pretix/static/pretixcontrol/js/ui/main.js:315
#: pretix/static/pretixcontrol/js/ui/main.js:316
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
"Il colore scelto non ha un buon contrasto, per favore scegline uno più scuro."
#: pretix/static/pretixcontrol/js/ui/main.js:461
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Tutto"
#: pretix/static/pretixcontrol/js/ui/main.js:462
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Nessuno"
#: pretix/static/pretixcontrol/js/ui/main.js:463
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Chiave di ricerca"
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Solo i selezionati"
#: pretix/static/pretixcontrol/js/ui/main.js:869
#: pretix/static/pretixcontrol/js/ui/main.js:871
msgid "Use a different name internally"
msgstr "Utilizza un nome diverso internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:905
#: pretix/static/pretixcontrol/js/ui/main.js:907
msgid "Click to close"
msgstr "Clicca per chiudere"
#: pretix/static/pretixcontrol/js/ui/main.js:977
#: pretix/static/pretixcontrol/js/ui/main.js:979
msgid "You have unsaved changes!"
msgstr "Hai cambiamenti non salvati!"
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