Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
2b40c9f44c Bump django-oauth-toolkit to 2.2.* 2022-12-17 20:08:12 +01:00
314 changed files with 119477 additions and 164419 deletions

View File

@@ -9,7 +9,6 @@ updates:
directory: "/src"
schedule:
interval: "daily"
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:

View File

@@ -17,9 +17,6 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
spelling:
name: Spellcheck

View File

@@ -15,9 +15,6 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
compile:
runs-on: ubuntu-22.04

View File

@@ -15,9 +15,6 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
isort:
name: isort

View File

@@ -15,9 +15,6 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
@@ -81,6 +78,5 @@ jobs:
uses: codecov/codecov-action@v1
with:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.10'

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

View File

@@ -14,5 +14,4 @@ This documentation is for everyone who wants to install pretix on a server.
maintainance
scaling
errors
mysql2postgres
indexes

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

View File

@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 11.6** but it should work very similar on other
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones.
Requirements
@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+ 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,6 +140,10 @@ We now install pretix, its direct dependencies and gunicorn::
(venv)$ pip3 install pretix gunicorn
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::
@@ -301,32 +318,12 @@ example::
(venv)$ python -m pretix rebuild
# systemctl restart pretix-web pretix-worker
System updates
--------------
After system updates, such as updates to a new Ubuntu or Debian release, you might be using a new Python version.
That's great, but requires some adjustments. First, adjust any old version paths in your nginx configuration file.
Then, re-create your Python environment::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 freeze > /tmp/pip-backup.txt
$ rm -rf /var/pretix/venv
$ python3 -m venv /var/pretix/venv
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip wheel setuptools
(venv)$ pip3 install -r /tmp/pip-backup.txt
Then, proceed like after any plugin installation::
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _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

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

View File

@@ -1,148 +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
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.

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)

View File

@@ -48,11 +48,10 @@ Possible permissions are:
Compatibility
-------------
We try to avoid any breaking changes to our API to avoid hassle on your end. If possible, we'll
build new features in a way that keeps all pre-existing API usage unchanged. In some cases,
this might not be possible or only possible with restrictions. In these case, any
backwards-incompatible changes will be prominently noted in the "Changes to the REST API"
section of our release notes. If possible, we will announce them multiple releases in advance.
We currently see pretix' API as a beta-stage feature. We therefore do not give any guarantees
for compatibility between feature releases of pretix (such as 1.5 and 1.6). However, as always,
we try not to break things when we don't need to. Any backwards-incompatible changes will be
prominently noted in the release notes.
We treat the following types of changes as *backwards-compatible* so we ask you to make sure
that your clients can deal with them properly:
@@ -61,7 +60,6 @@ that your clients can deal with them properly:
* Support of new HTTP methods for a given API endpoint
* Support of new query parameters for a given API endpoint
* New fields contained in API responses
* New possible values of enumeration-like fields
* Response body structure or message texts on failed requests (``4xx``, ``5xx`` response codes)
We treat the following types of changes as *backwards-incompatible*:
@@ -192,9 +190,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
specifiers in requests
(see below).
Date range *either* two dates separated ``2022-03-18/2022-03-23``, ``2022-03-18/``,
by ``/`` *or* the name of a ``/2022-03-23``, ``week_this``, ``week_next``,
defined range. ``month_this``
===================== ============================ ===================================
Query parameters

View File

@@ -98,8 +98,6 @@ Endpoints
:query string ends_after: Exclude all check-in lists attached to a sub-event that is already in the past at the given time.
:query string expand: Expand a field into a full object. Currently only ``subevent`` is supported. Can be passed multiple times.
:query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``name``, and ``subevent__date_from``,
Default: ``subevent__date_from,name``
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error

View File

@@ -35,12 +35,6 @@ tax_rule integer The internal ID
admission boolean ``true`` for items that grant admission to the event
(such as primary tickets) and ``false`` for others
(such as add-ons or merchandise).
personalized boolean ``true`` for items that require personalization according
to event settings. Only affects system-level fields, not
custom questions. Currently only allowed for products with
``admission`` set to ``true``. For backwards compatibility,
when creating new items and this field is not given, it defaults
to the same value as ``admission``.
position integer An integer, used for sorting
picture file A product picture to be displayed in the shop
(can be ``null``).
@@ -164,7 +158,7 @@ meta_data object Values set for
.. versionchanged:: 4.16
The ``variations[x].meta_data`` attribute has been added. The ``personalized`` attribute has been added.
The ``variations[x].meta_data`` attribute has been added.
Notes
-----
@@ -219,7 +213,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"personalized": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
@@ -336,7 +329,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"personalized": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
@@ -434,7 +426,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"personalized": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
@@ -519,7 +510,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"personalized": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,
@@ -636,7 +626,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"personalized": false,
"issue_giftcard": false,
"meta_data": {},
"position": 0,

View File

@@ -76,10 +76,6 @@ The exporter class
This is an abstract attribute, you **must** override this!
.. autoattribute:: description
.. autoattribute:: category
.. autoattribute:: export_form_fields
.. automethod:: render

View File

@@ -17,13 +17,9 @@ Field Type Description
id integer Internal layout ID
name string Internal layout description
default boolean ``true`` if this is the default layout
layout list Dynamic layout specification. Each list element
corresponds to one dynamic element of the layout.
The current version of the schema in use can be found
`here`_.
Submitting invalid content can lead to application errors.
layout object Layout specification for libpretixprint
background URL Background PDF file
item_assignments list of objects Products this layout is assigned to (currently read-only)
item_assignments list of objects Products this layout is assigned to
├ sales_channel string Sales channel (defaults to ``web``).
└ item integer Item ID
===================================== ========================== =======================================================
@@ -62,7 +58,7 @@ Endpoints
"name": "Default layout",
"default": true,
"layout": {…},
"background": null,
"background": {},
"item_assignments": []
}
]
@@ -100,7 +96,7 @@ Endpoints
"name": "Default layout",
"default": true,
"layout": {…},
"background": null,
"background": {},
"item_assignments": []
}
@@ -151,122 +147,3 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
Creates a new ticket layout
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
:param organizer: The ``slug`` field of the organizer of the event to create a layout for
:param event: The ``slug`` field of the event to create a layout for
:statuscode 201: no error
:statuscode 400: The layout could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
Update a layout. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "Default layout"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Default layout",
"default": true,
"layout": […],
"background": null,
"item_assignments": []
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the layout to modify
:statuscode 200: no error
:statuscode 400: The layout could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
Delete a layout.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the layout to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json

View File

@@ -97,7 +97,6 @@ overpayment
param
passphrase
percental
personalization
pluggable
positionid
pre

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.16.1"
__version__ = "4.16.0.dev0"

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={

View File

@@ -22,10 +22,8 @@
from django import forms
from django.http import QueryDict
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
class FormFieldWrapperField(serializers.Field):
@@ -144,12 +142,6 @@ class JobRunSerializer(serializers.Serializer):
allow_null=not v.required,
validators=v.validators,
)
elif isinstance(v, DateFrameField):
self.fields[k] = SerializerDateFrameField(
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
else:
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
@@ -159,40 +151,5 @@ class JobRunSerializer(serializers.Serializer):
for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data:
data[k] = []
for fk in self.fields.keys():
# Backwards compatibility for exports that used to take e.g. (date_from, date_to) or (event_date_from, event_date_to)
# and now only take date_range.
if fk.endswith("_range") and isinstance(self.fields[fk], SerializerDateFrameField) and fk not in data:
if fk.replace("_range", "_from") in data:
d_from = data.pop(fk.replace("_range", "_from"))
if d_from:
d_from = serializers.DateField().to_internal_value(d_from)
else:
d_from = None
if fk.replace("_range", "_to") in data:
d_to = data.pop(fk.replace("_range", "_to"))
if d_to:
d_to = serializers.DateField().to_internal_value(d_to)
else:
d_to = None
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
data = super().to_internal_value(data)
return data
def is_valid(self, raise_exception=False):
super().is_valid(raise_exception=raise_exception)
fields_keys = set(self.fields.keys())
input_keys = set(self.initial_data.keys())
additional_fields = input_keys - fields_keys
if bool(additional_fields):
self._errors['fields'] = ['Additional fields not allowed: {}.'.format(list(additional_fields))]
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)

View File

@@ -95,12 +95,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
require_membership_types = validated_data.pop('require_membership_types', [])
variation = ItemVariation.objects.create(**validated_data)
if require_membership_types:
variation.require_membership_types.add(*require_membership_types)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
@@ -234,7 +230,7 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'personalized',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
@@ -262,15 +258,6 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
if data.get('personalized') and not data.get('admission'):
raise ValidationError(_('Only admission products can currently be personalized.'))
if data.get('admission') and 'personalized' not in data and not self.instance:
# Backwards compatibility
data['personalized'] = True
elif 'admission' in data and not data['admission']:
data['personalized'] = False
if data.get('issue_giftcard'):
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
raise ValidationError(

View File

@@ -118,10 +118,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and not isinstance(data.get('name_parts'), dict):
raise ValidationError({'name_parts': ['Invalid data type']})
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
@@ -640,7 +636,7 @@ class OrderSerializer(I18nAwareModelSerializer):
for fname, field in list(self.fields.items()):
if fname in includes:
continue
elif hasattr(field, 'child'): # Nested list serializers
elif hasattr(field, 'child'):
found_any = False
for childfname, childfield in list(field.child.fields.items()):
if f'{fname}.{childfname}' not in includes:
@@ -649,15 +645,6 @@ class OrderSerializer(I18nAwareModelSerializer):
found_any = True
if not found_any:
self.fields.pop(fname)
elif isinstance(field, serializers.Serializer): # Nested serializers
found_any = False
for childfname, childfield in list(field.fields.items()):
if f'{fname}.{childfname}' not in includes:
field.fields.pop(childfname)
else:
found_any = True
if not found_any:
self.fields.pop(fname)
else:
self.fields.pop(fname)
@@ -845,10 +832,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
if data.get('attendee_name_parts') and not isinstance(data.get('attendee_name_parts'), dict):
raise ValidationError({'attendee_name_parts': ['Invalid data type']})
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme

View File

@@ -158,14 +158,12 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
a.question_id: a for a in instance.answers.all()
}
for answ_data in answers_data:
if not answ_data.get('answer'):
continue
options = answ_data.pop('options', [])
if answ_data['question'].pk in qs_seen:
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
if answ_data['question'].pk in answercache:
a = answercache[answ_data['question'].pk]
if isinstance(answ_data.get('answer'), File):
if isinstance(answ_data['answer'], File):
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
a.answer = 'file://' + a.file.name
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
@@ -175,7 +173,7 @@ class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
setattr(a, attr, value)
a.save()
else:
if isinstance(answ_data.get('answer'), File):
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
a = instance.answers.create(**answ_data, answer='')
a.file.save(os.path.basename(an.name), an, save=False)

View File

@@ -79,13 +79,6 @@ class CustomerSerializer(I18nAwareModelSerializer):
validated_data['external_identifier'] = instance.external_identifier
return super().update(instance, validated_data)
def validate(self, data):
if data.get('name_parts') and not isinstance(data.get('name_parts'), dict):
raise ValidationError({'name_parts': ['Invalid data type']})
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].organizer.settings.name_scheme
return data
class CustomerCreateSerializer(CustomerSerializer):
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)

View File

@@ -35,8 +35,7 @@
import importlib
from django.apps import apps
from django.conf.urls import re_path
from django.urls import include
from django.conf.urls import include, re_path
from rest_framework import routers
from pretix.api.views import cart

View File

@@ -93,10 +93,8 @@ with scopes_disabled():
class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
queryset = CheckinList.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
filter_backends = (DjangoFilterBackend,)
filterset_class = CheckinListFilter
ordering = ('subevent__date_from', 'name', 'id')
ordering_fields = ('subevent__date_from', 'id', 'name',)
def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'):
@@ -684,7 +682,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',

View File

@@ -34,7 +34,6 @@ from oauth2_provider.views import (
from pretix.api.models import OAuthApplication
from pretix.base.models import Organizer
from pretix.control.views.user import RecentAuthenticationRequiredMixin
logger = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ class OAuthAllowForm(AllowForm):
del self.fields['organizers']
class AuthorizationView(RecentAuthenticationRequiredMixin, BaseAuthorizationView):
class AuthorizationView(BaseAuthorizationView):
template_name = "pretixcontrol/auth/oauth_authorization.html"
form_class = OAuthAllowForm

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')
)

View File

@@ -21,7 +21,6 @@
#
import datetime
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.timezone import now
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
@@ -34,9 +33,6 @@ from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
from pretix.api.auth.token import TeamTokenAuthentication
from pretix.base.models import CachedFile
from pretix.helpers.images import (
IMAGE_TYPES, validate_uploaded_file_for_valid_image,
)
ALLOWED_TYPES = {
'image/gif': {'.gif'},
@@ -65,13 +61,6 @@ class UploadView(APIView):
name=file_obj.name,
type=content_type
))
if content_type in IMAGE_TYPES:
try:
validate_uploaded_file_for_valid_image(file_obj)
except DjangoValidationError as e:
raise ValidationError(e.message)
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),

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
@@ -503,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),
@@ -517,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),

View File

@@ -42,6 +42,7 @@ from localflavor.fr.forms import FRZipCodeField
from localflavor.gb.forms import GBPostcodeField
from localflavor.gr.forms import GRPostalCodeField
from localflavor.hr.forms import HRPostalCodeField
from localflavor.id_.forms import IDPostCodeField
from localflavor.ie.forms import EircodeField
from localflavor.il.forms import ILPostalCodeField
from localflavor.in_.forms import INZipCodeField
@@ -79,8 +80,8 @@ COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED = {
# We don't presume this for countries we don't have knowledge about, there are countries in the
# world e.g. without zipcodes
'AR', 'AT', 'AU', 'BE', 'BR', 'CA', 'CH', 'CN', 'CU', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR',
'GB', 'GR', 'HR', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX', 'NL',
'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA',
'GB', 'GR', 'HR', 'ID', 'IE', 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'LT', 'LV', 'MA', 'MT', 'MX',
'NL', 'NO', 'NZ', 'PK', 'PL', 'PT', 'RO', 'RU', 'SE', 'SG', 'SI', 'SK', 'TR', 'UA', 'US', 'ZA',
}
@@ -166,6 +167,7 @@ _zip_code_fields = {
'GB': GBPostcodeField,
'GR': GRPostalCodeField,
'HR': HRPostalCodeField,
'ID': IDPostCodeField,
'IE': EircodeField,
'IL': ILPostalCodeField,
'IN': INZipCodeField,

View File

@@ -381,14 +381,6 @@ def base_placeholders(sender, **kwargs):
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalMailTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
@@ -520,20 +512,20 @@ def base_placeholders(sender, **kwargs):
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
'url_remove', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
@@ -588,7 +580,7 @@ def base_placeholders(sender, **kwargs):
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(

View File

@@ -36,7 +36,7 @@ import io
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
from typing import Optional, Tuple
from typing import Tuple
import pytz
from defusedcsv import csv
@@ -84,27 +84,6 @@ class BaseExporter:
"""
raise NotImplementedError() # NOQA
@property
def description(self) -> str:
"""
A description for this exporter.
"""
return ""
@property
def category(self) -> Optional[str]:
"""
A category name for this exporter, or ``None``.
"""
return None
@property
def featured(self) -> bool:
"""
If ``True``, this exporter will be highlighted.
"""
return False
@property
def identifier(self) -> str:
"""

View File

@@ -39,7 +39,7 @@ from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from pretix.base.models import QuestionAnswer
@@ -49,10 +49,7 @@ from ..signals import register_data_exporters
class AnswerFilesExporter(BaseExporter):
identifier = 'answerfiles'
verbose_name = _('Question answer file uploads')
category = pgettext_lazy('export_category', 'Order data')
description = _('Download a ZIP file including all files that have been uploaded by your customers while creating '
'an order.')
verbose_name = _('Answers to file upload questions')
@property
def export_form_fields(self):

View File

@@ -36,7 +36,7 @@ from collections import OrderedDict
from django.dispatch import receiver
from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from django.utils.translation import gettext as _, gettext_lazy
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -48,8 +48,6 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'customerlist'
verbose_name = gettext_lazy('Customer accounts')
organizer_required_permission = 'can_manage_customers'
category = pgettext_lazy('export_category', 'Customer accounts')
description = gettext_lazy('Download a spreadsheet of all currently registered customer accounts.')
@property
def additional_form_fields(self):

View File

@@ -23,24 +23,22 @@ import json
from collections import OrderedDict
from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
from django.utils.translation import gettext, gettext_lazy
from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter
from ..signals import register_data_exporters
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class DekodiNREIExporter(BaseExporter):
identifier = 'dekodi_nrei'
verbose_name = 'dekodi NREI (JSON)'
category = pgettext_lazy('export_category', 'Invoices')
description = gettext_lazy("Download invoices in a format that can be used by the dekodi NREI conversion software.")
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
@@ -115,7 +113,7 @@ class DekodiNREIExporter(BaseExporter):
'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '',
})
elif p.provider and p.provider.startswith('stripe'):
elif p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data)
payments.append({
'PTID': '81',
@@ -194,12 +192,17 @@ class DekodiNREIExporter(BaseExporter):
def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
if d_start:
qs = qs.filter(date__gte=d_start)
if d_end:
qs = qs.filter(date__lte=d_end)
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
jo = {
'Format': 'NREI',
@@ -215,14 +218,22 @@ class DekodiNREIExporter(BaseExporter):
def export_form_fields(self):
return OrderedDict(
[
('date_range',
DateFrameField(
label=gettext_lazy('Date range'),
include_future_frames=False,
('date_from',
forms.DateField(
label=gettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=gettext_lazy('Only include invoices issued in this time frame. Note that the invoice date does '
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=gettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
]
)

View File

@@ -35,7 +35,7 @@
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from ...control.forms.filter import get_all_payment_providers
from ..exporter import ListExporter
@@ -45,8 +45,6 @@ from ..signals import register_multievent_data_exporters
class EventDataExporter(ListExporter):
identifier = 'eventdata'
verbose_name = _('Event data')
category = pgettext_lazy('export_category', 'Event data')
description = _('Download a spreadsheet with information on all events in this organizer account.')
@cached_property
def providers(self):

View File

@@ -38,15 +38,13 @@ from collections import OrderedDict
from decimal import Decimal
from zipfile import ZipFile
import dateutil.parser
from django import forms
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import (
gettext, gettext_lazy as _, pgettext, pgettext_lazy,
)
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
@@ -59,24 +57,30 @@ from ..services.invoices import invoice_pdf_task
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
class InvoiceExporterMixin:
category = pgettext_lazy('export_category', 'Invoices')
@property
def invoice_exporter_form_fields(self):
return OrderedDict(
[
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include invoices issued in this time frame. Note that the invoice date does '
help_text=_('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
('payment_provider',
forms.ChoiceField(
label=_('Payment provider'),
@@ -108,12 +112,16 @@ class InvoiceExporterMixin:
)
)
qs = qs.filter(has_payment_with_provider=1)
if form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
if d_start:
qs = qs.filter(date__gte=d_start)
if d_end:
qs = qs.filter(date__lte=d_end)
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
return qs
@@ -121,7 +129,6 @@ class InvoiceExporterMixin:
class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices'
verbose_name = _('All invoices')
description = _('Download all invoices created by the system as a ZIP file of PDF files.')
def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False)
@@ -173,10 +180,6 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
identifier = 'invoicedata'
verbose_name = _('Invoice data')
description = _('Download a spreadsheet with the data of all invoices created by the system. The spreadsheet '
'includes two sheets, one with a line for every invoice, and one with a line for every position of '
'every invoice.')
featured = True
@property
def additional_form_fields(self):

View File

@@ -22,7 +22,7 @@
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter
@@ -48,8 +48,6 @@ def _min(a1, a2):
class ItemDataExporter(ListExporter):
identifier = 'itemdata'
verbose_name = _('Product data')
category = pgettext_lazy('export_category', 'Product data')
description = _('Download a spreadsheet with details about all products and variations.')
def iterate_list(self, form_data):
locales = self.event.settings.locales
@@ -75,7 +73,6 @@ class ItemDataExporter(ListExporter):
_("Free price input"),
_("Sales tax"),
_("Is an admission ticket"),
_("Personalized ticket"),
_("Generate tickets"),
_("Waiting list"),
_("Available from"),
@@ -147,7 +144,6 @@ class ItemDataExporter(ListExporter):
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
_("Yes") if i.personalized else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "",
date_format(_max(i.available_from, v.available_from).astimezone(self.timezone),
@@ -191,7 +187,6 @@ class ItemDataExporter(ListExporter):
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
_("Yes") if i.personalized else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "",
date_format(i.available_from.astimezone(self.timezone),

View File

@@ -38,8 +38,6 @@ from decimal import Decimal
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.functional import lazy
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
from ..exporter import BaseExporter
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
@@ -48,10 +46,7 @@ from ..signals import register_data_exporters
class JSONExporter(BaseExporter):
identifier = 'json'
verbose_name = lazy(lambda *args: gettext('Order data') + ' (JSON)', str)()
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a structured JSON representation of all orders. This might be useful for the '
'import in third-party systems.')
verbose_name = 'Order data (JSON)'
def render(self, form_data):
jo = {
@@ -83,7 +78,6 @@ class JSONExporter(BaseExporter):
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission,
'personalized': item.personalized,
'active': item.active,
'sales_channels': item.sales_channels,
'description': str(item.description),

View File

@@ -36,7 +36,7 @@ from collections import OrderedDict
from django import forms
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPosition
@@ -50,8 +50,6 @@ from ..signals import (
class MailExporter(BaseExporter):
identifier = 'mailaddrs'
verbose_name = _('Email addresses (text file)')
category = pgettext_lazy('export_category', 'Order data')
description = _("Download a text file with all email addresses collected either from buyers or from ticket holders.")
def render(self, form_data: dict):
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')

View File

@@ -33,8 +33,10 @@
# License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict
from datetime import date, datetime, time
from decimal import Decimal
import dateutil
import pytz
from django import forms
from django.db.models import (
@@ -44,10 +46,8 @@ from django.db.models import (
from django.db.models.functions import Coalesce
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
)
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
@@ -63,24 +63,14 @@ from ...helpers.iter import chunked_iterable
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
from ..forms.widgets import SplitDateTimePickerWidget
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
from ..timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist'
verbose_name = gettext_lazy('Order data')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all orders. The spreadsheet will include three sheets, one '
'with a line for every order, one with a line for every order position, and one with '
'a line for every additional fee charged in an order.')
featured = True
@cached_property
def providers(self):
@@ -115,25 +105,41 @@ class OrderListExporter(MultiSheetListExporter):
initial=False,
required=False
)),
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders created within this date range.')
help_text=_('Only include orders created on or after this date.')
)),
('event_date_range',
DateFrameField(
label=_('Event date'),
include_future_frames=True,
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date in this range. '
help_text=_('Only include orders created on or before this date.')
)),
('event_date_from',
forms.DateField(
label=_('Start event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or after this date. '
'Will also include other dates in case of mixed orders!')
)),
('event_date_to',
forms.DateField(
label=_('End event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or before this date. '
'Will also include other dates in case of mixed orders!')
)),
]
d = OrderedDict(d)
if not self.is_multievent and not self.event.has_subevents:
del d['event_date_range']
del d['event_date_from']
del d['event_date_to']
return d
def _get_all_payment_methods(self, qs):
@@ -176,27 +182,45 @@ class OrderListExporter(MultiSheetListExporter):
annotations = {}
filters = {}
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if dt_start:
filters[f'{rel}datetime__gte'] = dt_start
if dt_end:
filters[f'{rel}datetime__lt'] = dt_end
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
if form_data.get('event_date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone)
if dt_start:
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_max__gte'] = dt_start
if dt_end:
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_min__lt'] = dt_end
filters[f'{rel}datetime__gte'] = datetime_value
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
filters[f'{rel}datetime__lte'] = datetime_value
if form_data.get('event_date_from'):
date_value = form_data.get('event_date_from')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_max__gte'] = datetime_value
if form_data.get('event_date_to'):
date_value = form_data.get('event_date_to')
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_min__lte'] = datetime_value
if filters:
return qs.annotate(**annotations).filter(**filters)
@@ -752,10 +776,7 @@ class OrderListExporter(MultiSheetListExporter):
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = gettext_lazy('Payments and refunds')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds of every order.')
featured = True
verbose_name = gettext_lazy('Order payments and refunds')
@property
def additional_form_fields(self):
@@ -834,8 +855,6 @@ class PaymentListExporter(ListExporter):
class QuotaListExporter(ListExporter):
identifier = 'quotalist'
verbose_name = gettext_lazy('Quota availabilities')
category = pgettext_lazy('export_category', 'Product data')
description = gettext_lazy('Download a spreadsheet of all quotas including their current availability.')
def iterate_list(self, form_data):
has_subevents = self.event.has_subevents
@@ -889,17 +908,21 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
@property
def additional_form_fields(self):
d = [
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
required=False
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
]
d = OrderedDict(d)
@@ -910,12 +933,22 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if dt_start:
qs = qs.filter(datetime__gte=dt_start)
if dt_end:
qs = qs.filter(datetime__lt=dt_end)
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [
_('Gift card code'),
@@ -945,8 +978,6 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Gift card redemptions')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds that involve gift cards.')
def iterate_list(self, form_data):
payments = OrderPayment.objects.filter(
@@ -992,18 +1023,14 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
@property
def additional_form_fields(self):
return OrderedDict(
[
('date', forms.SplitDateTimeField(
('date', forms.DateTimeField(
label=_('Show value at'),
required=False,
widget=SplitDateTimePickerWidget(),
help_text=_('Defaults to the time of report.')
initial=now(),
)),
('testmode', forms.ChoiceField(
label=_('Test mode'),
@@ -1031,13 +1058,12 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
)
def iterate_list(self, form_data):
d = form_data.get('date') or now()
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk'),
datetime__lte=d
datetime__lte=form_data['date']
).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = self.organizer.issued_gift_cards.filter(
issuance__lte=d
issuance__lte=form_data['date']
).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
).order_by('issuance').prefetch_related(
@@ -1052,11 +1078,11 @@ class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
if form_data.get('state') == 'empty':
qs = qs.filter(cached_value=0)
elif form_data.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=d))
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=form_data['date']))
elif form_data.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=d)
qs = qs.exclude(cached_value=0).filter(expires__lt=form_data['date'])
elif form_data.get('state') == 'expired':
qs = qs.filter(expires__lt=d)
qs = qs.filter(expires__lt=form_data['date'])
headers = [
_('Gift card code'),

View File

@@ -39,8 +39,6 @@ from ..signals import (
class WaitingListExporter(ListExporter):
identifier = 'waitinglist'
verbose_name = _('Waiting list')
category = pgettext_lazy('export_category', 'Waiting list')
description = _('Download a spread sheet with all your waiting list data.')
# map selected status to label and queryset-filter
status_filters = [

View File

@@ -531,7 +531,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
code='aspect_ratio_not_3_by_4',
)
except Exception as exc:
logger.exception('Could not parse image')
logger.exception('foo')
# Pillow doesn't recognize it as an image.
if isinstance(exc, ValidationError):
raise
@@ -575,7 +575,7 @@ class BaseQuestionsForm(forms.Form):
add_fields = {}
if item.ask_attendee_data and event.settings.attendee_names_asked:
if item.admission and event.settings.attendee_names_asked:
add_fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required and not self.all_optional,
@@ -584,7 +584,7 @@ class BaseQuestionsForm(forms.Form):
label=_('Attendee name'),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.ask_attendee_data and event.settings.attendee_emails_asked:
if item.admission and event.settings.attendee_emails_asked:
add_fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'),
@@ -595,7 +595,7 @@ class BaseQuestionsForm(forms.Form):
}
)
)
if item.ask_attendee_data and event.settings.attendee_company_asked:
if item.admission and event.settings.attendee_company_asked:
add_fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
@@ -603,7 +603,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
if item.admission and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),

View File

@@ -30,6 +30,7 @@ from django.urls import get_script_prefix
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import (
check_for_language, get_supported_language_variant, language_code_re,
parse_accept_lang_header,
@@ -127,7 +128,12 @@ def get_language_from_user_settings(request: HttpRequest) -> str:
return lang_code
def get_language_from_cookie(request: HttpRequest) -> str:
def get_language_from_session_or_cookie(request: HttpRequest) -> str:
if hasattr(request, 'session'):
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
if lang_code in _supported and lang_code is not None and check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
try:
return get_supported_language_variant(lang_code)
@@ -181,14 +187,14 @@ def get_language_from_request(request: HttpRequest) -> str:
return (
get_language_from_user_settings(request)
or get_language_from_customer_settings(request)
or get_language_from_cookie(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
else:
return (
get_language_from_cookie(request)
get_language_from_session_or_cookie(request)
or get_language_from_customer_settings(request)
or get_language_from_user_settings(request)
or get_language_from_browser(request)
@@ -218,11 +224,6 @@ def _merge_csp(a, b):
if k not in a:
a[k] = b[k]
for k, v in a.items():
if "'unsafe-inline'" in v:
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
class SecurityMiddleware(MiddlewareMixin):
CSP_EXEMPT = (
@@ -300,7 +301,7 @@ class SecurityMiddleware(MiddlewareMixin):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain)
for k, v in h.items():
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']

View File

@@ -1,27 +0,0 @@
# Generated by Django 3.2.16 on 2022-12-21 08:59
from django.db import migrations, models
def item_set_personalized(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
Item = apps.get_model("pretixbase", "Item")
Item.objects.filter(admission=True).update(personalized=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0226_itemvariationmetavalue'),
]
operations = [
migrations.AddField(
model_name='item',
name='personalized',
field=models.BooleanField(default=False),
),
migrations.RunPython(
item_set_personalized,
migrations.RunPython.noop,
),
]

View File

@@ -1,68 +0,0 @@
# Generated by Django 3.2.16 on 2023-01-18 11:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0227_item_personalized'),
]
operations = [
migrations.CreateModel(
name='ScheduledOrganizerExport',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('export_identifier', models.CharField(max_length=190)),
('export_form_data', models.JSONField(default=dict)),
('locale', models.CharField(max_length=250)),
('mail_additional_recipients', models.TextField()),
('mail_additional_recipients_cc', models.TextField()),
('mail_additional_recipients_bcc', models.TextField()),
('mail_subject', models.CharField(max_length=250)),
('mail_template', models.TextField()),
('schedule_rrule', models.TextField(null=True)),
('schedule_rrule_time', models.TimeField()),
('schedule_next_run', models.DateTimeField(blank=True, null=True)),
('error_counter', models.IntegerField(default=0)),
('error_last_message', models.TextField(null=True)),
('timezone', models.CharField(default='UTC', max_length=100)),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_exports', to='pretixbase.organizer')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='ScheduledEventExport',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('export_identifier', models.CharField(max_length=190)),
('export_form_data', models.JSONField(default=dict)),
('locale', models.CharField(max_length=250)),
('mail_additional_recipients', models.TextField()),
('mail_additional_recipients_cc', models.TextField()),
('mail_additional_recipients_bcc', models.TextField()),
('mail_subject', models.CharField(max_length=250)),
('mail_template', models.TextField()),
('schedule_rrule', models.TextField(null=True)),
('schedule_rrule_time', models.TimeField()),
('schedule_next_run', models.DateTimeField(blank=True, null=True)),
('error_counter', models.IntegerField(default=0)),
('error_last_message', models.TextField(null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_exports', to='pretixbase.event')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]

View File

@@ -30,7 +30,6 @@ from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
SubEvent, SubEventMetaValue, generate_invite_token,
)
from .exports import ScheduledEventExport, ScheduledOrganizerExport
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (

View File

@@ -36,17 +36,13 @@ from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import (
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
)
from django.db.models.expressions import RawSQL
from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.helpers import PostgresWindowFrame
class CheckinList(LoggedModel):
@@ -99,109 +95,52 @@ 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:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
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'))
)
)
return qs
# 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
# a speed-up from 29s (old) to a few hundred milliseconds (new)!
# Why is this so much faster? The regular query get's PostgreSQL all busy with filtering
# the tickets both by their belonging the event and checkin status at the same time, while
# this query just iterates over all successful checkins on the list, and -- by the power
# of window functions -- asks "is this an entry that is followed by no exit?". Then we
# dedupliate by position and count it up.
cl = self
base_q, base_params = (
Checkin.all.filter(*c_q, successful=True, list=cl)
.annotate(
cnt_exists_after=Window(
expression=Count("position_id", filter=Q(type=Value("exit"))),
partition_by=[F("position_id"), F("list_id")],
order_by=F("datetime").asc(),
frame=PostgresWindowFrame(
"ROWS", start="1 following", end="unbounded following"
),
)
)
.values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params()
)
return self.positions_query(ignore_status=ignore_status).filter(
pk__in=RawSQL(
f"""
SELECT "position_id"
FROM ({str(base_q)}) s
WHERE "type" = %s AND "cnt_exists_after" = 0
GROUP BY "position_id"
""",
[*base_params, Checkin.TYPE_ENTRY]
)
)
@property
def positions_inside(self):
return self.positions_inside_query(None)
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'))
)
)
@property
def inside_count(self):
return self.positions_inside_query(None).count()
return self.positions_inside.count()
@property
@scopes_disabled()

View File

@@ -374,7 +374,7 @@ class EventMixin:
if q.active_items:
items_reserved.update(q.active_items.split(","))
if q.active_variations:
vars_reserved.update(q.active_variations.split(","))
vars_available.update(q.active_variations.split(","))
elif res[0] < Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_gone.update(q.active_items.split(","))
@@ -632,7 +632,6 @@ class Event(EventMixin, LoggedModel):
return super().presale_has_ended
def delete_all_orders(self, really=False):
from .checkin import Checkin
from .orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund, Transaction,
)
@@ -646,7 +645,6 @@ class Event(EventMixin, LoggedModel):
OrderFee.objects.filter(order__event=self).delete()
OrderRefund.objects.filter(order__event=self).delete()
OrderPayment.objects.filter(order__event=self).delete()
Checkin.objects.filter(list__event=self).delete()
self.orders.all().delete()
def save(self, *args, **kwargs):

View File

@@ -1,139 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
from datetime import datetime, timedelta
import pytz
from dateutil.rrule import rrulestr
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import LoggedModel
from pretix.base.validators import RRuleValidator, multimail_validate
class AbstractScheduledExport(LoggedModel):
id = models.BigAutoField(primary_key=True)
export_identifier = models.CharField(
max_length=190,
verbose_name=_("Export"),
)
export_form_data = models.JSONField(
default=dict,
encoder=DjangoJSONEncoder,
)
owner = models.ForeignKey(
"pretixbase.User",
on_delete=models.PROTECT,
)
locale = models.CharField(
verbose_name=_('Language'),
max_length=250
)
mail_additional_recipients = models.TextField(
verbose_name=_('Additional recipients'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_additional_recipients_cc = models.TextField(
verbose_name=_('Additional recipients (Cc)'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_additional_recipients_bcc = models.TextField(
verbose_name=_('Additional recipients (Bcc)'),
null=False, blank=True, validators=[multimail_validate],
help_text=_("You can specify multiple recipients separated by commas.")
)
mail_subject = models.CharField(
verbose_name=_('Subject'),
max_length=250
)
mail_template = models.TextField(
verbose_name=_('Message'),
)
schedule_rrule = models.TextField(
null=True, blank=True, validators=[RRuleValidator()]
)
schedule_rrule_time = models.TimeField(
verbose_name=_("Requested start time"),
help_text=_("The actual start time might be delayed depending on system load."),
)
schedule_next_run = models.DateTimeField(null=True, blank=True)
error_counter = models.IntegerField(default=0)
error_last_message = models.TextField(null=True, blank=True)
class Meta:
abstract = True
def __str__(self):
return self.mail_subject
def compute_next_run(self):
tz = self.tz
r = rrulestr(self.schedule_rrule)
base_dt = now().astimezone(tz).replace(tzinfo=None)
if now().astimezone(tz).time() < self.schedule_rrule_time:
base_dt -= timedelta(days=1)
new_d = r.after(base_dt, inc=False)
if not new_d:
self.schedule_next_run = None
return
try:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
except pytz.exceptions.AmbiguousTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz, is_dst=False)
except pytz.exceptions.NonExistentTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time) + timedelta(hours=1), tz)
class ScheduledEventExport(AbstractScheduledExport):
event = models.ForeignKey(
"pretixbase.Event", on_delete=models.CASCADE, related_name="scheduled_exports"
)
@property
def tz(self):
return self.event.timezone
class ScheduledOrganizerExport(AbstractScheduledExport):
organizer = models.ForeignKey(
"pretixbase.Organizer", on_delete=models.CASCADE, related_name="scheduled_exports"
)
timezone = models.CharField(max_length=100,
default=settings.TIME_ZONE,
verbose_name=_('Timezone'))
@property
def tz(self):
return pytz.timezone(self.timezone)

View File

@@ -62,7 +62,6 @@ from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from ...helpers.images import ImageSizeValidator
from .event import Event, SubEvent
@@ -311,8 +310,6 @@ class Item(LoggedModel):
:type tax_rate: decimal.Decimal
:param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
:type admission: bool
:param personalized: ``True``, if attendee information should be collected for this ticket
:type personalized: bool
:param picture: A product picture to be shown next to the product description
:type picture: File
:param available_from: The date this product goes on sale
@@ -399,14 +396,8 @@ class Item(LoggedModel):
admission = models.BooleanField(
verbose_name=_("Is an admission ticket"),
help_text=_(
'Whether or not buying this product allows a person to enter your event'
),
default=False
)
personalized = models.BooleanField(
verbose_name=_("Is a personalized ticket"),
help_text=_(
'Whether or not buying this product allows to enter attendee information'
'Whether or not buying this product allows a person to enter '
'your event'
),
default=False
)
@@ -430,8 +421,7 @@ class Item(LoggedModel):
picture = models.ImageField(
verbose_name=_("Product picture"),
null=True, blank=True, max_length=255,
upload_to=itempicture_upload_to,
validators=[ImageSizeValidator()]
upload_to=itempicture_upload_to
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
@@ -588,10 +578,6 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left
return self.show_quota_left
@property
def ask_attendee_data(self):
return self.admission and self.personalized
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price

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 (
@@ -809,7 +808,7 @@ class Order(LockModel, LoggedModel):
return True
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in positions:
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
if (cp.item.admission and ask_names) or cp.item.questions.all():
return True
return False # nothing there to modify
@@ -1629,7 +1628,7 @@ 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(
@@ -1674,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(
@@ -2689,7 +2688,7 @@ class CartPosition(AbstractPosition):
category_key = (self.item.category.position, self.item.category.id) if self.item.category_id is not None else (0, 0)
item_key = self.item.position, self.item_id
variation_key = (self.variation.position, self.variation.id) if self.variation_id is not None else (0, 0)
line_key = (self.price, (self.voucher_id or 0), (self.seat.sorting_rank if self.seat_id else 0), self.pk)
line_key = (self.price, (self.voucher_id or 0), (self.seat.sorting_rank if self.seat_id else None), self.pk)
sort_key = subevent_key + category_key + item_key + variation_key + line_key
if self.addon_to_id:

View File

@@ -23,7 +23,6 @@ import json
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _, pgettext
@@ -150,15 +149,7 @@ class TaxRule(LoggedModel):
rate = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[
MaxValueValidator(
limit_value=Decimal("100.00"),
),
MinValueValidator(
limit_value=Decimal("0.00"),
),
],
verbose_name=_("Tax rate"),
verbose_name=_("Tax rate")
)
price_includes_tax = models.BooleanField(
verbose_name=_("The configured product prices include the tax amount"),

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
from datetime import timedelta
from typing import Any, Dict, Union
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction
@@ -28,16 +27,14 @@ from django.db.models import F, Q, Sum
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from i18nfield.strings import LazyI18nString
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...helpers.format import format_map
from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation
@@ -216,74 +213,15 @@ class WaitingListEntry(LoggedModel):
self.voucher = v
self.save()
self.send_mail(
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(
event=self.event,
waiting_list_entry=self,
waiting_list_voucher=v,
event_or_subevent=self.subevent or self.event,
),
user=user,
auth=auth,
)
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.waitinglist.email.sent',
user: User=None, headers: dict=None, sender: str=None, auth=None, auto_email=True,
attach_other_files: list=None, attach_cached_files: list=None):
"""
Sends an email to the entry's contact address.
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, and ``recipient``
parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
"""
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
recipient = self.email
try:
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event,
self.locale,
headers=headers,
sender=sender,
auto_email=auto_email,
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
}
)
mail(
self.email,
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(event=self.event, waiting_list_entry=self),
self.event,
locale=self.locale
)
@staticmethod
def clean_itemvar(event, item, variation):

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
@@ -326,6 +325,16 @@ class BasePaymentProvider:
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
)),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
'This will only be used if the invoice is generated before the order is paid. If the '
'invoice is generated later, it will show a text stating that it has already been paid.'),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
('_total_min',
forms.DecimalField(
label=_('Minimum order total'),
@@ -373,16 +382,6 @@ class BasePaymentProvider:
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
'This will only be used if the invoice is generated before the order is paid. If the '
'invoice is generated later, it will show a text stating that it has already been paid.'),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
('_restricted_countries',
forms.MultipleChoiceField(
label=_('Restrict to countries'),
@@ -1400,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

View File

@@ -35,7 +35,6 @@
import copy
import hashlib
import itertools
import json
import logging
import os
import re
@@ -47,15 +46,12 @@ from collections import OrderedDict
from functools import partial
from io import BytesIO
import jsonschema
from arabic_reshaper import ArabicReshaper
from bidi.algorithm import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Max, Min
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format
from django.utils.functional import SimpleLazyObject
from django.utils.html import conditional_escape
@@ -744,9 +740,9 @@ class Renderer:
if o['content'] == 'other' or o['content'] == 'other_i18n':
if o['content'] == 'other_i18n':
text = str(LazyI18nString(o.get('text_i18n', {})))
text = str(LazyI18nString(o['text_i18n']))
else:
text = o.get('text', '')
text = o['text']
def replace(x):
if x.group(1).startswith('itemmeta:'):
@@ -979,22 +975,3 @@ class Renderer:
output.write(outbuffer)
outbuffer.seek(0)
return outbuffer
@deconstructible
class PdfLayoutValidator:
def __call__(self, value):
if not isinstance(value, dict):
try:
val = json.loads(value)
except ValueError:
raise ValidationError(_('Your layout file is not a valid JSON file.'))
else:
val = value
with open(finders.find('schema/pdf-layout.schema.json'), 'r') as f:
schema = json.loads(f.read())
try:
jsonschema.validate(val, schema)
except jsonschema.ValidationError as e:
e = str(e).replace('%', '%%')
raise ValidationError(_('Your layout file is not a valid layout. Error message: {}').format(e))

View File

@@ -65,14 +65,7 @@ def get_all_plugins(event=None) -> List[type]:
)
class PluginConfigMeta(type):
def __getattribute__(cls, item):
if item == "default" and cls is PluginConfig:
return False
return super().__getattribute__(item)
class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
class PluginConfig(AppConfig):
IGNORE = False
def __init__(self, *args, **kwargs):

View File

@@ -59,10 +59,10 @@ class RelativeDateWrapper:
def date(self, event) -> datetime.date:
from .models import SubEvent
if isinstance(self.data, datetime.datetime):
return self.data.date()
elif isinstance(self.data, datetime.date):
if isinstance(self.data, datetime.date):
return self.data
elif isinstance(self.data, datetime.datetime):
return self.data.date()
else:
if self.data.minutes_before is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')

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 = []

View File

@@ -140,7 +140,7 @@ error_messages = {
'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.'),
'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
'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.'),

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(

View File

@@ -19,50 +19,31 @@
# 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/>.
#
import logging
from datetime import timedelta
from typing import Any, Dict, Union
from typing import Any, Dict
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import connection, transaction
from django.dispatch import receiver
from django.utils.timezone import now, override
from django.utils.timezone import override
from django.utils.translation import gettext
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
User, cachedfile_name,
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
)
from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail
from pretix.base.services.tasks import (
EventTask, OrganizerTask, ProfiledEventTask, ProfiledOrganizerUserTask,
ProfiledEventTask, ProfiledOrganizerUserTask,
)
from pretix.base.signals import (
periodic_task, register_data_exporters, register_multievent_data_exporters,
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__)
class ExportError(LazyLocaleException):
pass
class ExportEmptyError(ExportError):
pass
@app.task(base=ProfiledEventTask, throws=(ExportError,), bind=True)
def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
def set_progress(val):
@@ -75,7 +56,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for recv, response in responses:
for receiver, response in responses:
if not response:
continue
ex = response(event, event.organizer, set_progress)
@@ -125,16 +106,16 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = organizer.settings.timezone or settings.TIME_ZONE
region = organizer.settings.region
with language(locale, region), override(timezone):
if form_data.get('events') is not None and not form_data.get('all_events'):
if form_data.get('events') is not None:
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=form_data.get('events'), organizer=organizer)
events = allowed_events.filter(pk__in=form_data.get('events'))
else:
events = allowed_events.filter(organizer=organizer)
events = allowed_events
responses = register_multievent_data_exporters.send(organizer)
for recv, response in responses:
for receiver, response in responses:
if not response:
continue
ex = response(events, organizer, set_progress)
@@ -157,210 +138,3 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
with language(schedule.locale, context.settings.region), override(schedule.tz):
file = CachedFile(web_download=False)
file.date = now()
file.expires = now() + timedelta(hours=24)
file.save()
def _handle_error(msg, soft=False):
context.log_action(
'pretix.event.export.schedule.failed',
data={
'id': schedule.id,
'export_identifier': schedule.export_identifier,
'export_form_data': schedule.export_form_data,
'reason': msg,
'soft': soft,
}
)
if schedule.owner.is_active:
mail(
email=schedule.owner.email,
subject=gettext('Export failed'),
template='pretixbase/email/export_failed.txt',
context={
'configuration_url': config_url,
'reason': msg,
'soft': soft,
},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
locale=schedule.locale,
)
if not soft:
schedule.error_counter += 1
schedule.error_last_message = msg
schedule.save(update_fields=['error_counter', 'error_last_message'])
if not has_permission:
_handle_error(gettext('Permission denied.'))
return
try:
if not exporter:
raise ExportError("Export type not found.")
d = exporter.render(schedule.export_form_data)
if d is None:
raise ExportEmptyError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
filesize = len(data)
if filesize > 20 * 1024 * 1024: # 20 MB
raise ExportError(
gettext('Your exported data exceeded the size limit for scheduled exports.')
)
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
except ExportEmptyError as e:
_handle_error(str(e), soft=True)
except ExportError as e:
_handle_error(str(e), soft=False)
except Exception:
logger.exception("Scheduled export failed.")
try:
retry_func()
except MaxRetriesExceededError:
_handle_error('Internal Error')
else:
schedule.error_counter = 0
schedule.save(update_fields=['error_counter'])
to = [r for r in schedule.mail_additional_recipients.split(",") if r]
cc = [r for r in schedule.mail_additional_recipients_cc.split(",") if r]
bcc = [r for r in schedule.mail_additional_recipients_bcc.split(",") if r]
if to:
# If there is an explicit To, the owner is Cc. Otherwise, the owner is To. Yes, this is
# purely cosmetical and has policital reasons.
cc.append(schedule.owner.email)
else:
to.append(schedule.owner.email)
mail(
email=to,
cc=cc,
bcc=bcc,
subject=schedule.mail_subject,
template=LazyI18nString(schedule.mail_template),
context=get_email_context(event=context) if isinstance(context, Event) else {},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
locale=schedule.locale,
attach_cached_files=[file],
)
context.log_action(
'pretix.event.export.schedule.executed',
data={
'id': schedule.id,
'export_identifier': schedule.export_identifier,
'export_form_data': schedule.export_form_data,
'result_file_size': filesize,
'result_file_name': file.file.name,
}
)
@app.task(base=OrganizerTask, bind=True, max_retries=5, default_retry_delay=120)
def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> None:
schedule = organizer.scheduled_exports.get(pk=schedule)
allowed_events = schedule.owner.get_events_with_permission('can_view_orders')
if schedule.export_form_data.get('events') is not None and not schedule.export_form_data.get('all_events'):
if isinstance(schedule.export_form_data['events'][0], str):
events = allowed_events.filter(slug__in=schedule.export_form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=schedule.export_form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(organizer=organizer)
responses = register_multievent_data_exporters.send(organizer)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(events, organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
has_permission = schedule.owner.is_active
if isinstance(exporter, OrganizerLevelExportMixin):
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
has_permission = False
_run_scheduled_export(
schedule,
organizer,
exporter,
build_absolute_uri(
'control:organizer.export',
kwargs={
'organizer': organizer.slug,
}
) + f'?identifier={schedule.export_identifier}&scheduled={schedule.pk}',
self.retry,
has_permission,
)
@app.task(base=EventTask, bind=True, max_retries=5, default_retry_delay=120)
def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule = event.scheduled_exports.get(pk=schedule)
responses = register_data_exporters.send(event)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(event, event.organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'can_view_orders')
_run_scheduled_export(
schedule,
event,
exporter,
build_absolute_uri(
'control:event.orders.export',
kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}
) + f'?identifier={schedule.export_identifier}&scheduled={schedule.pk}',
self.retry,
has_permission,
)
@receiver(signal=periodic_task)
@scopes_disabled()
@transaction.atomic
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')
for s in qs:
scheduled_event_export.apply_async(kwargs={
'event': s.event_id,
'schedule': s.pk,
})
s.compute_next_run()
s.save(update_fields=['schedule_next_run'])
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')
for s in qs:
scheduled_organizer_export.apply_async(kwargs={
'organizer': s.organizer_id,
'schedule': s.pk,
})
s.compute_next_run()
s.save(update_fields=['schedule_next_run'])

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__)
@@ -452,7 +452,7 @@ def build_preview_invoice_pdf(event):
if event.tax_rules.exists():
for i, tr in enumerate(event.tax_rules.all()):
for j in range(5):
for j in range(150):
tax = tr.tax(Decimal('100.00'), base_price_is='gross')
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product {}").format(i + 1),
@@ -460,7 +460,7 @@ def build_preview_invoice_pdf(event):
tax_rate=tax.rate
)
else:
for i in range(5):
for i in range(150):
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"),
gross_value=100, tax_value=0, tax_rate=0
@@ -500,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):

View File

@@ -86,6 +86,7 @@ INVALID_ADDRESS = 'invalid-pretix-mail-address'
class TolerantDict(dict):
# kept for backwards compatibility with plugins
def __missing__(self, key):
return key
@@ -99,8 +100,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None):
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -150,21 +150,12 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
:param attach_other_files: A list of file paths on our storage to attach.
:param plain_text_only: If set to ``True``, rendering a HTML version will be skipped.
:param no_order_links: If set to ``True``, no link to the order confirmation page will be auto-appended. Currently
only allowed to use together with ``plain_text_only`` since HTML renderers add their own
links.
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
if email == INVALID_ADDRESS:
return
if no_order_links and not plain_text_only:
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
headers = headers or {}
if auto_email:
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
@@ -188,7 +179,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
})
renderer = ClassicMailRenderer(None, organizer)
content_plain = body_plain = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(str(subject), context)
sender = (
sender or
(event.settings.get('mail_from') if event else None) or
@@ -211,7 +202,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
signature = ""
bcc = list(bcc or [])
bcc = []
settings_holder = event or organizer
@@ -253,7 +244,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if order and order.testmode:
subject = "[TESTMODE] " + subject
if order and position and not no_order_links:
if order and position:
body_plain += _(
"You are receiving this email because someone placed an order for {event} for you."
).format(event=event.name)
@@ -269,7 +260,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
}
)
)
elif order and not no_order_links:
elif order:
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
@@ -289,9 +280,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
with override(timezone):
try:
if plain_text_only:
body_html = None
elif 'position' in inspect.signature(renderer.render).parameters:
if 'position' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
@@ -305,7 +294,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email),
cc=cc,
bcc=bcc,
subject=subject,
body=body_plain,
@@ -358,11 +346,11 @@ class CustomEmail(EmailMultiAlternatives):
@app.task(base=TransactionAwareTask, bind=True, acks_late=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int = None, position: int = None, headers: dict = None, cc: List[str] = None, bcc: List[str] = None,
event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None,
invoices: List[int] = None, order: int = None, attach_tickets=False, user=None,
organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None,
attach_other_files: List[str] = None) -> bool:
email = CustomEmail(subject, body, sender, to=to, cc=cc, bcc=bcc, headers=headers)
email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
html_with_cid, cid_images = replace_images_with_cid_paths(html)

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')\

View File

@@ -97,7 +97,6 @@ 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
@@ -185,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
@@ -398,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):
@@ -420,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 '
@@ -1069,8 +1068,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
any_payment_failed = True
except Exception:
logger.exception('Error during payment attempt')
else:
order.refresh_from_db()
pending_sum = order.pending_sum
free_order_flow = (
@@ -1206,7 +1203,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
@@ -1265,7 +1262,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
@@ -2060,7 +2057,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 '
@@ -2076,7 +2073,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 '
@@ -2649,9 +2646,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

View File

@@ -109,31 +109,6 @@ class EventTask(app.Task):
return ret
class OrganizerTask(app.Task):
def __call__(self, *args, **kwargs):
if 'organizer_id' in kwargs:
organizer_id = kwargs.get('organizer_id')
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
del kwargs['organizer_id']
kwargs['organizer'] = organizer
elif 'organizer' in kwargs:
organizer_id = kwargs.get('organizer')
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
kwargs['organizer'] = organizer
else:
args = list(args)
organizer_id = args[0]
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
args[0] = organizer
with scope(organizer=organizer):
ret = super().__call__(*args, **kwargs)
return ret
class OrganizerUserTask(app.Task):
def __call__(self, *args, **kwargs):
organizer_id = kwargs['organizer']

View File

@@ -206,7 +206,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all personalized tickets."),
help_text=_("Ask for a name for all tickets which include admission to the event."),
)
},
'attendee_names_required': {
@@ -229,10 +229,10 @@ DEFAULTS = {
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every personalized ticket. This might be useful if you want to "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the email settings."),
"the per-attendee addresses. You can however enable this in the E-mail settings."),
)
},
'attendee_emails_required': {
@@ -242,7 +242,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require email addresses per ticket"),
help_text=_("Require customers to fill in individual email addresses for all personalized tickets. See the "
help_text=_("Require customers to fill in individual e-mail addresses for all admission tickets. See the "
"above option for more details. One email address for the order confirmation will always be "
"required regardless of this setting."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
@@ -2574,7 +2574,7 @@ Your {organizer} team"""))
label=_("Attendee data explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain "
help_text=_("This text will be shown above the questions asked for every admission product. You can use it e.g. to explain "
"why you need information from them.")
)
},

View File

@@ -218,10 +218,8 @@ class EmailAddressShredder(BaseDataShredder):
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer'])
for le in self.event.logentry_set.filter(
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"),
):
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
for le in self.event.logentry_set.filter(action_type__contains="order.email"):
shred_log_fields(le, banlist=['recipient', 'message', 'subject'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
shred_log_fields(le, banlist=['old_email', 'new_email'])

View File

@@ -1,12 +0,0 @@
{% load i18n %}
{% trans "Your export failed." %}
{% trans "Reason:" %} {{ reason }}
{% if not soft %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "Configuration link:" %}
{{ configuration_url }}

View File

@@ -1,18 +0,0 @@
{% load i18n %}
{% with widget.subwidgets.0 as widget %}
{% include widget.template_name %}
{% endwith %}
<div class="row" data-display-dependency-value="custom" data-display-dependency="#{{ widget.subwidgets.0.attrs.id }}">
<br>
<div class="col-sm-6">
{% with widget.subwidgets.1 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
<div class="col-sm-6">
{% with widget.subwidgets.2 as widget %}
{% include widget.template_name %}
{% endwith %}
</div>
</div>
{% spaceless %}{% for widget in widget.subwidgets %}{% endfor %}{% endspaceless %}

View File

@@ -1,433 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import calendar
from datetime import date, datetime, time, timedelta
from itertools import groupby
from typing import Optional, Tuple
import pytz
from django import forms
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy, pgettext_lazy
from rest_framework import serializers
from pretix.helpers.daterange import daterange
def _quarter_start(ref_d):
return ref_d.replace(day=1, month=1 + (ref_d.month - 1) // 3 * 3)
def _week_start(ref_d):
return ref_d - timedelta(ref_d.weekday())
REPORTING_DATE_TIMEFRAMES = (
# (identifier, label, start_inclusive, end_inclusive, includes_future, optgroup, describe)
(
'days_today',
pgettext_lazy('reporting_timeframe', 'Today'),
lambda ref_d: ref_d,
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_yesterday',
pgettext_lazy('reporting_timeframe', 'Yesterday'),
lambda ref_d: ref_d - timedelta(days=1),
lambda ref_d, start_d: start_d,
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last7',
pgettext_lazy('reporting_timeframe', 'Last 7 days'),
lambda ref_d: ref_d - timedelta(days=6),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_last14',
pgettext_lazy('reporting_timeframe', 'Last 14 days'),
lambda ref_d: ref_d - timedelta(days=13),
lambda ref_d, start_d: start_d + timedelta(days=13),
False,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_tomorrow',
pgettext_lazy('reporting_timeframe', 'Tomorrow'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d,
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next7',
pgettext_lazy('reporting_timeframe', 'Next 7 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'days_next14',
pgettext_lazy('reporting_timeframe', 'Next 14 days'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: start_d + timedelta(days=13),
True,
pgettext_lazy('reporting_timeframe', 'by day'),
daterange
),
(
'week_this',
pgettext_lazy('reporting_timeframe', 'Current week'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_to_date',
pgettext_lazy('reporting_timeframe', 'Current week to date'),
lambda ref_d: _week_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_previous',
pgettext_lazy('reporting_timeframe', 'Previous week'),
lambda ref_d: _week_start(ref_d) - timedelta(days=7),
lambda ref_d, start_d: start_d + timedelta(days=6),
False,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'week_next',
pgettext_lazy('reporting_timeframe', 'Next week'),
lambda ref_d: _week_start(ref_d + timedelta(days=7)),
lambda ref_d, start_d: start_d + timedelta(days=6),
True,
pgettext_lazy('reporting_timeframe', 'by week'),
lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
),
(
'month_this',
pgettext_lazy('reporting_timeframe', 'Current month'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_to_date',
pgettext_lazy('reporting_timeframe', 'Current month to date'),
lambda ref_d: ref_d.replace(day=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_previous',
pgettext_lazy('reporting_timeframe', 'Previous month'),
lambda ref_d: (ref_d.replace(day=1) - timedelta(days=1)).replace(day=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
False,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'month_next',
pgettext_lazy('reporting_timeframe', 'Next month'),
lambda ref_d: ref_d.replace(day=calendar.monthrange(ref_d.year, ref_d.month)[1]) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
True,
pgettext_lazy('reporting_timeframe', 'by month'),
lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
),
(
'quarter_this',
pgettext_lazy('reporting_timeframe', 'Current quarter'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_to_date',
pgettext_lazy('reporting_timeframe', 'Current quarter to date'),
lambda ref_d: _quarter_start(ref_d),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_previous',
pgettext_lazy('reporting_timeframe', 'Previous quarter'),
lambda ref_d: _quarter_start(_quarter_start(ref_d) - timedelta(days=1)),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
False,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'quarter_next',
pgettext_lazy('reporting_timeframe', 'Next quarter'),
lambda ref_d: ref_d.replace(
day=calendar.monthrange(ref_d.year, _quarter_start(ref_d).month + 2)[1], month=_quarter_start(ref_d).month + 2
) + timedelta(days=1),
lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
True,
pgettext_lazy('reporting_timeframe', 'by quarter'),
lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
),
(
'year_this',
pgettext_lazy('reporting_timeframe', 'Current year'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_to_date',
pgettext_lazy('reporting_timeframe', 'Current year to date'),
lambda ref_d: ref_d.replace(day=1, month=1),
lambda ref_d, start_d: ref_d,
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_previous',
pgettext_lazy('reporting_timeframe', 'Previous year'),
lambda ref_d: (ref_d.replace(day=1, month=1) - timedelta(days=1)).replace(day=1, month=1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
False,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'year_next',
pgettext_lazy('reporting_timeframe', 'Next year'),
lambda ref_d: ref_d.replace(day=1, month=1, year=ref_d.year + 1),
lambda ref_d, start_d: start_d.replace(day=31, month=12),
True,
pgettext_lazy('reporting_timeframe', 'by year'),
lambda start_d, end_d: str(start_d.year),
),
(
'future',
pgettext_lazy('reporting_timeframe', 'All future (excluding today)'),
lambda ref_d: ref_d + timedelta(days=1),
lambda ref_d, start_d: None,
True,
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: date_format(start_d, "SHORT_DATE_FORMAT") + ' ',
),
(
'past',
pgettext_lazy('reporting_timeframe', 'All past (including today)'),
lambda ref_d: None,
lambda ref_d, start_d: ref_d,
True, # technically false, but only makes sense to have in a selection that also allows the future, otherwise redundant
pgettext_lazy('reporting_timeframe', 'Other'),
lambda start_d, end_d: ' ' + date_format(end_d, "SHORT_DATE_FORMAT"),
),
)
class DateFrameWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/dateframe.html'
def __init__(self, *args, **kwargs):
self.timeframe_choices = kwargs.pop('timeframe_choices')
widgets = (
forms.Select(choices=self.timeframe_choices),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'Start')}),
forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'End')}),
)
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
if not value:
return ['unset', None, None]
if '/' in value:
return [
'custom',
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'
return ctx
def _describe_timeframe(label, start, end, future, describe):
d_start = start(now())
d_end = end(now(), d_start)
details = describe(d_start, d_end)
return f'{label} ({details})'
class DateFrameField(forms.MultiValueField):
default_error_messages = {
**forms.MultiValueField.default_error_messages,
'inconsistent': gettext_lazy('The end date must be after the start date.'),
}
def __init__(self, *args, **kwargs):
include_future_frames = kwargs.pop('include_future_frames')
top_choices = [('custom', gettext_lazy('Custom timeframe'))]
if not kwargs.get('required', True):
top_choices.insert(0, ('unset', pgettext_lazy('reporting_timeframe', 'All time')))
_choices = []
for grouper, group in groupby(REPORTING_DATE_TIMEFRAMES, key=lambda i: i[5]):
options = [
(identifier, _describe_timeframe(label, start, end, future, describe))
for identifier, label, start, end, future, group, describe in group
if include_future_frames or not future
]
if options:
_choices.append((grouper, options))
timeframe_choices = [
('', top_choices)
] + _choices
fields = (
forms.ChoiceField(
choices=timeframe_choices,
required=True
),
forms.DateField(
required=False
),
forms.DateField(
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = DateFrameWidget(timeframe_choices=timeframe_choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
def compress(self, data_list):
if not data_list:
return None
if data_list[0] == 'unset':
return None
elif data_list[0] == 'custom':
return f'{data_list[1].isoformat() if data_list[1] else ""}/{data_list[2].isoformat() if data_list[2] else ""}'
else:
return data_list[0]
def has_changed(self, initial, data):
if initial is None:
initial = self.widget.decompress(initial)
return super().has_changed(initial, data)
def clean(self, value):
if not value:
return None
if value[0] == 'custom':
if not value[1] and not value[2]:
raise ValidationError(self.error_messages['incomplete'])
if value[1] and value[2] and self.fields[2].to_python(value[2]) < self.fields[1].to_python(value[1]):
raise ValidationError(self.error_messages['inconsistent'])
return super().clean(value)
class SerializerDateFrameField(serializers.CharField):
def to_internal_value(self, data):
if data is None:
return None
try:
resolve_timeframe_to_dates_inclusive(now(), data, pytz.UTC)
except:
raise ValidationError("Invalid date frame")
def to_representation(self, value):
if value is None:
return None
return value
def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of dates
where the first element ist the first possible date value within the timeframe and the second
element is the last possible date value in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
if isinstance(ref_dt, datetime):
ref_dt = ref_dt.astimezone(timezone).date()
if "/" in frame:
start, end = frame.split("/", 1)
return date.fromisoformat(start) if start else None, date.fromisoformat(end) if end else None
for idf, label, start, end, includes_future, *args in REPORTING_DATE_TIMEFRAMES:
if frame == idf:
d_start = start(ref_dt)
d_end = end(ref_dt, d_start)
return d_start, d_end
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second
element is the first possible datetime value *not* in the timeframe.
Both returned values may be ``None`` for an unlimited interval.
"""
d_start, d_end = resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone)
dt_start = make_aware(datetime.combine(d_start, time(0, 0, 0)), timezone) if d_start else None
dt_end = make_aware(datetime.combine(d_end + timedelta(days=1), time(0, 0, 0)), timezone) if d_end else None
return dt_start, dt_end

View File

@@ -19,12 +19,6 @@
# 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/>.
#
from dateutil.rrule import rrulestr
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
@@ -38,6 +32,11 @@ from django.utils.translation import gettext_lazy as _
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
class BanlistValidator:
@@ -102,18 +101,3 @@ class EmailBanlistValidator(BanlistValidator):
banlist = [
settings.PRETIX_EMAIL_NONE_VALUE,
]
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
class RRuleValidator:
def __call__(self, value):
try:
rrulestr(value)
except Exception:
raise ValidationError("Not a valid rrule.")

View File

@@ -78,7 +78,7 @@ class BaseQuestionsViewMixin:
form.pos = cartpos or orderpos
form.show_copy_answers_to_addon_button = form.pos.addon_to and (
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
(form.pos.addon_to.item.ask_attendee_data and form.pos.item.ask_attendee_data and (
(form.pos.addon_to.item.admission and form.pos.item.admission and (
self.request.event.settings.attendee_names_asked or
self.request.event.settings.attendee_emails_asked or
self.request.event.settings.attendee_company_asked or

View File

@@ -28,7 +28,7 @@ from django.urls import reverse
def _is_samesite_referer(request):
referer = request.headers.get('referer')
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False

View File

@@ -28,7 +28,7 @@ from celery import states
from celery.result import AsyncResult
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.exceptions import ValidationError
from django.http import HttpResponse, JsonResponse, QueryDict
from django.shortcuts import redirect, render
from django.test import RequestFactory
@@ -149,8 +149,6 @@ class AsyncMixin:
return redirect(self.get_success_url(value))
def error(self, exception):
if isinstance(exception, PermissionDenied):
raise exception
messages.error(self.request, self.get_error_message(exception))
if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
@@ -339,8 +337,8 @@ class AsyncPostView(AsyncMixin, View):
depend on the request object unless specifically supported by this class. File upload is currently also
not supported.
"""
known_errortypes = ['ValidationError', 'PermissionDenied']
expected_exceptions = (ValidationError, PermissionDenied)
known_errortypes = ['ValidationError']
expected_exceptions = (ValidationError,)
task_base = ProfiledEventTask
def async_set_progress(self, percentage):

View File

@@ -51,9 +51,6 @@ from django_scopes.forms import SafeModelMultipleChoiceField
from pretix.helpers.hierarkey import clean_filename
from ...base.forms import I18nModelForm
from ...helpers.images import (
IMAGE_EXTS, validate_uploaded_file_for_valid_image,
)
# Import for backwards compatibility with okd import paths
from ...base.forms.widgets import ( # noqa
@@ -217,16 +214,12 @@ class ExtValidationMixin:
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs)
if isinstance(data, UploadedFile):
if isinstance(data, File):
filename = data.name
ext = os.path.splitext(filename)[1]
ext = ext.lower()
if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!"))
if ext in IMAGE_EXTS:
validate_uploaded_file_for_valid_image(data)
return data

View File

@@ -34,13 +34,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from decimal import Decimal
from urllib.parse import urlencode, urlparse
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator
from django.core.validators import MaxValueValidator, validate_email
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import (
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
@@ -66,7 +65,6 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
SplitDateTimePickerWidget,
@@ -137,8 +135,6 @@ class EventWizardBasicsForm(I18nModelForm):
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
"here in percent. If you have a more complicated tax situation, you can add more tax rates and "
"detailed configuration later."),
max_value=Decimal("100.00"),
min_value=Decimal("0.00"),
required=False
)
@@ -865,6 +861,13 @@ class InvoiceSettingsForm(SettingsForm):
return data
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
def contains_web_channel_validate(val):
if "web" not in val:
raise ValidationError(_("The online shop must be selected to receive these emails."))
@@ -1203,8 +1206,8 @@ class MailSettingsForm(SettingsForm):
'mail_text_resend_link': ['event', 'order'],
'mail_subject_resend_link': ['event', 'order'],
'mail_subject_resend_link_attendee': ['event', 'order'],
'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
'mail_subject_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'],
'mail_text_waiting_list': ['event', 'waiting_list_entry'],
'mail_subject_waiting_list': ['event', 'waiting_list_entry'],
'mail_text_resend_all_links': ['event', 'orders'],
'mail_subject_resend_all_links': ['event', 'orders'],
'mail_attach_ical_description': ['event', 'event_or_subevent'],

View File

@@ -1,103 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import ScheduledEventExport
from pretix.base.models.exports import ScheduledOrganizerExport
class ScheduledEventExportForm(forms.ModelForm):
class Meta:
model = ScheduledEventExport
fields = ['mail_additional_recipients', 'mail_additional_recipients_cc', 'mail_additional_recipients_bcc',
'mail_subject', 'mail_template', 'schedule_rrule_time', 'locale']
widgets = {
'mail_additional_recipients': forms.TextInput,
'mail_additional_recipients_cc': forms.TextInput,
'mail_additional_recipients_bcc': forms.TextInput,
'schedule_rrule_time': forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'] = forms.ChoiceField(
label=_('Language'),
choices=[(a, locale_names[a]) for a in self.instance.event.settings.locales]
)
def clean_mail_additional_recipients(self):
d = self.cleaned_data['mail_additional_recipients'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
def clean_mail_additional_recipients_cc(self):
d = self.cleaned_data['mail_additional_recipients_cc'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
def clean_mail_additional_recipients_bcc(self):
d = self.cleaned_data['mail_additional_recipients_bcc'].replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError(_('Please enter less than 25 recipients.'))
return d
class ScheduledOrganizerExportForm(forms.ModelForm):
class Meta:
model = ScheduledOrganizerExport
fields = ['mail_additional_recipients', 'mail_additional_recipients_cc', 'mail_additional_recipients_bcc',
'mail_subject', 'mail_template', 'schedule_rrule_time', 'locale', 'timezone']
widgets = {
'mail_additional_recipients': forms.TextInput,
'mail_additional_recipients_cc': forms.TextInput,
'mail_additional_recipients_bcc': forms.TextInput,
'schedule_rrule_time': forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'] = forms.ChoiceField(
label=_('Language'),
choices=[(a, locale_names[a]) for a in self.instance.organizer.settings.locales]
)
self.fields['timezone'] = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Timezone"),
)
def clean_mail_additional_recipients(self):
return self.cleaned_data['mail_additional_recipients'].replace(' ', '')
def clean_mail_additional_recipients_cc(self):
return self.cleaned_data['mail_additional_recipients_cc'].replace(' ', '')
def clean_mail_additional_recipients_bcc(self):
return self.cleaned_data['mail_additional_recipients_bcc'].replace(' ', '')

View File

@@ -77,31 +77,6 @@ def get_all_payment_providers():
if PAYMENT_PROVIDERS:
return PAYMENT_PROVIDERS
class FakeSettings:
def __init__(self, orig_settings):
self.orig_settings = orig_settings
def set(self, *args, **kwargs):
pass
def __getattr__(self, item):
return getattr(self.orig_settings, item)
class FakeEvent:
def __init__(self, orig_event):
self.orig_event = orig_event
@property
def settings(self):
return FakeSettings(self.orig_event.settings)
def __getattr__(self, item):
return getattr(self.orig_event, item)
@property
def __class__(self): # hackhack
return Event
with rolledback_transaction():
event = Event.objects.create(
plugins=",".join([app.name for app in apps.get_app_configs()]),
@@ -109,7 +84,6 @@ def get_all_payment_providers():
date_from=now(),
organizer=Organizer.objects.create(name="INTERNAL")
)
event = FakeEvent(event)
provs = register_payment_providers.send(
sender=event
)
@@ -792,12 +766,6 @@ class OrderSearchFilterForm(OrderFilterForm):
)
)
def use_query_hack(self):
return (
self.cleaned_data.get('query') or
self.cleaned_data.get('status') in ('overpaid', 'partially_paid', 'underpaid', 'pendingpaid')
)
def filter_qs(self, qs):
fdata = self.cleaned_data
qs = super().filter_qs(qs)
@@ -1703,7 +1671,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'))
@@ -2262,7 +2232,7 @@ class DeviceFilterForm(FilterForm):
state = forms.ChoiceField(
label=_('Device status'),
choices=[
('all', _('All devices')),
('', _('All devices')),
('active', _('Active devices')),
('revoked', _('Revoked devices'))
],

View File

@@ -295,7 +295,6 @@ class ItemCreateForm(I18nModelForm):
self.user = kwargs.pop('user')
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault('admission', True)
kwargs['initial'].setdefault('personalized', True)
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
@@ -404,8 +403,6 @@ class ItemCreateForm(I18nModelForm):
self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
if not self.instance.admission:
self.instance.personalized = False
instance = super().save(*args, **kwargs)
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
@@ -497,7 +494,6 @@ class ItemCreateForm(I18nModelForm):
'internal_name',
'category',
'admission',
'personalized',
'default_price',
'tax_rule',
]
@@ -592,33 +588,13 @@ class ItemUpdateForm(I18nModelForm):
'tax_rule',
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
)
if d.get('admission'):
if d['admission']:
self.add_error(
'admission',
_(
"Gift card products should not be admission products at the same time."
)
)
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
_(
"If a valid membership is required, at least one valid membership type needs to be selected."
)
)
if not d.get('admission'):
d['personalized'] = False
if d.get('grant_membership_type'):
if not d['grant_membership_type'].transferable and not d['personalized']:
self.add_error(
'personalized' if d['admission'] else 'admission',
_("Your product grants a non-transferable membership and should therefore be a personalized "
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
"want the membership to be non-personalized, set the membership type to be transferable.")
)
return d
def clean_picture(self):
@@ -639,7 +615,6 @@ class ItemUpdateForm(I18nModelForm):
'active',
'sales_channels',
'admission',
'personalized',
'description',
'picture',
'default_price',
@@ -817,17 +792,6 @@ class ItemVariationForm(I18nModelForm):
}),
}
def clean(self):
d = super().clean()
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
_(
"If a valid membership is required, at least one valid membership type needs to be selected."
)
)
return d
def save(self, commit=True):
instance = super().save(commit)
self.meta_fields = []

View File

@@ -227,10 +227,6 @@ class ExporterForm(forms.Form):
elif isinstance(v, models.QuerySet):
data[k] = [m.pk for m in v]
if 'all_events' in self.fields and 'events' in self.fields:
if not data.get('all_events') and not data.get('events'):
raise ValidationError(_('Please select some events.'))
return data
@@ -270,7 +266,7 @@ class OtherOperationsForm(forms.Form):
notify = forms.BooleanField(
label=_('Notify user'),
required=False,
initial=False,
initial=True,
help_text=_(
'Send an email to the customer notifying that their order has been changed.'
)

View File

@@ -1,251 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
from datetime import timedelta
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rrulestr
from django import forms
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
],
initial='weekly'
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1,
min_value=1,
widget=forms.NumberInput(attrs={'min': '1'})
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().astimezone(get_current_timezone()).date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
('forever', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repetitions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=30)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
(str(i), MONTHS[i]) for i in range(1, 13)
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
def to_rrule(self):
rule_kwargs = {}
rule_kwargs['dtstart'] = self.cleaned_data['dtstart']
rule_kwargs['interval'] = self.cleaned_data['interval']
if self.cleaned_data['freq'] == 'yearly':
freq = YEARLY
if self.cleaned_data['yearly_same'] == "off":
rule_kwargs['bysetpos'] = int(self.cleaned_data['yearly_bysetpos'])
rule_kwargs['byweekday'] = self.parse_weekdays(self.cleaned_data['yearly_byweekday'])
rule_kwargs['bymonth'] = int(self.cleaned_data['yearly_bymonth'])
elif self.cleaned_data['freq'] == 'monthly':
freq = MONTHLY
if self.cleaned_data['monthly_same'] == "off":
rule_kwargs['bysetpos'] = int(self.cleaned_data['monthly_bysetpos'])
rule_kwargs['byweekday'] = self.parse_weekdays(self.cleaned_data['monthly_byweekday'])
elif self.cleaned_data['freq'] == 'weekly':
freq = WEEKLY
if self.cleaned_data['weekly_byweekday']:
rule_kwargs['byweekday'] = [self.parse_weekdays(a) for a in self.cleaned_data['weekly_byweekday']]
elif self.cleaned_data['freq'] == 'daily':
freq = DAILY
if self.cleaned_data['end'] == 'count':
rule_kwargs['count'] = self.cleaned_data['count']
elif self.cleaned_data['end'] == 'until':
rule_kwargs['until'] = self.cleaned_data['until']
return rrule(freq, **rule_kwargs)
@staticmethod
def initial_from_rrule(rule: rrule):
initial = {}
if isinstance(rule, str):
rule = rrulestr(rule)
_rule = rule._original_rule
initial['dtstart'] = rule._dtstart
initial['interval'] = rule._interval
if rule._freq == YEARLY:
initial['freq'] = 'yearly'
initial['yearly_bysetpos'] = _rule.get('bysetpos')
initial['yearly_byweekday'] = _rule.get('byweekday')
initial['yearly_bymonth'] = _rule.get('bymonth')
elif rule._freq == MONTHLY:
initial['freq'] = 'monthly'
initial['monthly_bysetpos'] = _rule.get('bysetpos')
initial['monthly_byweekday'] = _rule.get('byweekday')
elif rule._freq == WEEKLY:
initial['freq'] = 'weekly'
initial['weekly_byweekday'] = _rule.get('byweekday')
elif rule._freq == DAILY:
initial['freq'] = 'daily'
if rule._count:
initial['end'] = 'count'
initial['count'] = rule._count
elif rule._until:
initial['end'] = 'until'
initial['until'] = rule._until
else:
initial['end'] = 'forever'
return initial

View File

@@ -19,15 +19,17 @@
# 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/>.
#
from datetime import datetime
from datetime import datetime, timedelta
from urllib.parse import urlencode
from django import forms
from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
@@ -37,7 +39,6 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.rrule import RRuleForm
from pretix.helpers.money import change_decimal_field
@@ -439,15 +440,166 @@ class CheckinListFormSet(I18nInlineFormSet):
return form
class RRuleFormSetForm(RRuleForm):
class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
exclude = forms.BooleanField(
label=_('Exclude these dates instead of adding them.'),
required=False
)
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
],
initial='weekly'
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1,
min_value=1,
widget=forms.NumberInput(attrs={'min': '1'})
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repetitions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=30)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
(str(i), MONTHS[i]) for i in range(1, 13)
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', WEEKDAYS[0]),
('TU', WEEKDAYS[1]),
('WE', WEEKDAYS[2]),
('TH', WEEKDAYS[3]),
('FR', WEEKDAYS[4]),
('SA', WEEKDAYS[5]),
('SU', WEEKDAYS[6]),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
RRuleFormSet = formset_factory(
RRuleFormSetForm,
RRuleForm,
can_order=False, can_delete=True, extra=1
)

View File

@@ -315,11 +315,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'),
'pretix.organizer.export.schedule.added': _('A scheduled export has been added.'),
'pretix.organizer.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.webhook.created': _('The webhook has been created.'),
@@ -414,11 +409,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'),
'pretix.event.export.schedule.added': _('A scheduled export has been added.'),
'pretix.event.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.event.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.event.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.event.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),

View File

@@ -112,7 +112,7 @@ class PermissionMiddleware:
url = resolve(request.path_info)
url_name = url.url_name
if not request.path.startswith(get_script_prefix() + 'control') and not (url.namespace.startswith("api-") and url_name == "authorize"):
if not request.path.startswith(get_script_prefix() + 'control'):
# This middleware should only touch the /control subpath
return self.get_response(request)

View File

@@ -40,7 +40,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/rrule.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/variations.js" %}"></script>
@@ -423,17 +422,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." %}

View File

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

View File

@@ -82,10 +82,8 @@
<thead>
<tr>
<th>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
@@ -127,7 +125,7 @@
{% for e in entries %}
<tr>
<td>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
{% if "can_change_orders" in request.eventpermset %}
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
{% endif %}
</td>
@@ -200,7 +198,7 @@
</tbody>
</table>
</div>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Check-In selected attendees" %}
@@ -209,8 +207,6 @@
<span class="fa fa-sign-out" aria-hidden="true"></span>
{% trans "Check-Out selected attendees" %}
</button>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-danger btn-save" name="revert" value="true">
<span class="fa fa-trash" aria-hidden="true"></span>
{% trans "Delete all check-ins of selected attendees" %}

View File

@@ -36,16 +36,12 @@
</div>
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
{% if request.event %}
{% for l, n in settings.LANGUAGES %}
{% if l in request.event.settings.locales %}
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endif %}
{% for l in request.event.settings.locales %}
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endfor %}
{% else %}
{% for l, n in settings.LANGUAGES %}
{% if l in request.organizer.settings.locales %}
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endif %}
{% for l in request.organizer.settings.locales %}
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
{% endfor %}
{% endif %}
</div>

View File

@@ -90,7 +90,7 @@
</div>
</div>
<h4>{% trans "Attendee data (once per personalized ticket)" %}</h4>
<h4>{% trans "Attendee data (once per admission ticket)" %}</h4>
{% bootstrap_field sform.attendee_names_asked_required layout="control" %}
{% bootstrap_field sform.attendee_emails_asked_required layout="control" %}

View File

@@ -20,19 +20,13 @@
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on">
<span class="fa fa-fw fa-user"></span>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, we will only offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -46,12 +40,12 @@
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-cube"></span>
<span class="fa fa-cube"></span>
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, we will not offer ticket downloads
(but you can still enable ticket downloads in event settings or product settings).
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -64,47 +58,6 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-circle-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.category layout="control" %}
<div class="form-group">

View File

@@ -27,19 +27,13 @@
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on">
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-fw fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, we will only offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -57,8 +51,8 @@
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, we will not offer ticket downloads
(but you can still enable ticket downloads in event settings or product settings).
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -71,52 +65,6 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
{% for e in form.errors.personalized %}
<div class="alert alert-danger has-error">
{{ e }}
</div>
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-file-text-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.picture layout="control" %}
{% bootstrap_field form.require_approval layout="control" %}

View File

@@ -81,11 +81,7 @@
</td>
<td>
{% if i.admission %}
{% if i.personalized %}
<span class="fa fa-id-card-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Personalized admission ticket" %}"></span>
{% else %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket without personalization" %}"></span>
{% endif %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
{% elif i.issue_giftcard %}
<span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span>
{% endif %}

View File

@@ -76,7 +76,7 @@
{% endfor %}
</ul>
{% else %}
<small>{% trans "All personalized products" %}</small>
<small>{% trans "All admission products" %}</small>
{% endif %}
</td>
<td class="dnd-container">

View File

@@ -184,11 +184,9 @@
<dt>{% trans "Order locale" %}</dt>
<dd>
{{ display_locale }}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</dd>
{% if order.status == "n" %}
<dt>{% trans "Expiry date" %}</dt>
@@ -208,11 +206,9 @@
{{ order.customer.identifier }} {{ order.customer.email }}
</a>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</dd>
{% endif %}
<dt>{% trans "Contact email" %}</dt>
@@ -221,23 +217,21 @@
{% if order.email and order.email_known_to_work %}
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
{% if order.email %}
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a>
{% if order.email %}
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-envelope-o"></span>
</a>
{% if order.status != "c" %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<button class="btn btn-default btn-xs">
{% trans "Resend link" %}
</button>
</form>
{% endif %}
{% if order.status != "c" %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<button class="btn btn-default btn-xs">
{% trans "Resend link" %}
</button>
</form>
{% endif %}
{% endif %}
</dd>
@@ -245,11 +239,9 @@
<dt>{% trans "Phone number" %}</dt>
<dd>
{{ order.phone|default_if_none:""|phone_format }}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</dd>
{% endif %}
{% if invoices %}
@@ -314,7 +306,7 @@
{% trans "Email invoices" %}
</a>
{% endif %}
{% if can_generate_invoice and 'can_change_orders' in request.eventpermset %}
{% if can_generate_invoice %}
<br/>
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
@@ -325,7 +317,7 @@
</form>
{% endif %}
</dd>
{% elif can_generate_invoice and 'can_change_orders' in request.eventpermset %}
{% elif can_generate_invoice %}
<dt>{% trans "Invoices" %}</dt>
<dd>
<form class="form-inline helper-display-inline" method="post"
@@ -343,11 +335,11 @@
<div class="panel panel-default items">
<div class="panel-heading">
<div class="pull-right flip">
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a>
{% if 'can_change_orders' in request.eventpermset %}
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
</a>
&middot; <a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change products" %}
@@ -468,12 +460,12 @@
{% endif %}
{% if line.has_questions %}
<dl>
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
{% if line.item.admission and event.settings.attendee_names_asked %}
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
{% if line.item.admission and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt>
<dd>
{% if line.attendee_email %}
@@ -496,7 +488,7 @@
{% endif %}
</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
{% if line.item.admission and event.settings.attendee_company_asked %}
<dt>
{% trans "Attendee company" %}
</dt>
@@ -504,7 +496,7 @@
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
{% if line.item.admission and event.settings.attendee_addresses_asked %}
<dt>
{% trans "Attendee address" %}
</dt>
@@ -762,7 +754,7 @@
{% endfor %}
</tbody>
</table>
{% if order.payment_refund_sum > 0 and "can_change_orders" in request.eventpermset %}
{% if order.payment_refund_sum > 0 %}
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
{% trans "Create a refund" %}
</a>
@@ -950,11 +942,9 @@
{% bootstrap_field comment_form.comment show_help=True show_label=False %}
{% bootstrap_field comment_form.custom_followup_at %}
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
{% if "can_change_orders" in request.eventpermset %}
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
{% endif %}
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
</form>
</div>
</div>

View File

@@ -4,113 +4,35 @@
{% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>
<h1>
{% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1>
{% if scheduled %}
<h2>{% trans "Scheduled exports" %}</h2>
<ul class="list-group">
{% for s in scheduled %}
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-5 col-md-4 col-xs-12">
<span class="fa fa-fw fa-folder"></span>
{{ s.export_verbose_name }}
<br>
<span class="text-muted">
<span class="fa fa-fw fa-user"></span>
{{ s.owner.fullname|default:s.owner.email }}
</span>
</div>
<div class="col-lg-5 col-md-6 col-xs-12">
{% if s.schedule_next_run %}
<span class="fa fa-clock-o fa-fw"></span>
{% trans "Next run:" %}
{{ s.schedule_next_run|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
<span class="fa fa-clock-o fa-fw"></span>
{% trans "No next run scheduled" %}
{% endif %}
{% if s.export_verbose_name == "?" %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Exporter not found" %}
</strong>
{% elif s.error_counter >= 5 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Disabled due to multiple failures" %}
</strong>
{% elif s.error_counter > 0 %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Failed recently" %}
</strong>
{% endif %}
<span class="text-muted">
<br>
<span class="fa fa-fw fa-envelope-o"></span>
{{ s.mail_subject }}
</span>
</div>
<div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ s.export_identifier }}"/>
<input type="hidden" name="scheduled" value="{{ s.pk }}"/>
{% if s.export_verbose_name != "?" %}
<button type="submit" class="btn btn-default" title="{% trans "Run export now" %}" data-toggle="tooltip">
<span class="fa fa-download"></span>
</button>
<button formaction="{% url "control:event.orders.export.scheduled.run" organizer=request.organizer.slug event=request.event.slug pk=s.pk %}"
type="submit"
title="{% trans "Run export and send via email now. This will not change the next scheduled execution." %}"
data-toggle="tooltip" class="btn btn-default" data-no-asynctask>
<span class="fa fa-play" aria-hidden="true"></span>
</button>
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span>
</a>
{% endif %}
<a href="{% url "control:event.orders.export.scheduled.delete" event=request.event.slug organizer=request.event.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span>
</a>
</form>
</div>
</div>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endif %}
{% regroup exporters by category as category_list %}
{% for c, c_ex in category_list %}
{% if c %}
<h2>{{ c }}</h2>
{% else %}
<h2>{% trans "Other exports" %}</h2>
{% endif %}
<div class="list-group large-link-group">
{% for e in c_ex %}
<a class="list-group-item" href="?identifier={{ e.identifier }}">
<h4>
{{ e.verbose_name }}
{% if e.featured %}
<span class="fa fa-star text-success" data-toggle="tooltip"
title="{% trans "Recommended for new users" %}" aria-hidden="true"></span>
{% endif %}
</h4>
{% if e.description %}
<p>
{{ e.description }}
</p>
{% endif %}
</a>
{% endfor %}
</div>
{% for e in exporters %}
<details class="panel panel-default"
{% if request.GET.identifier == e.identifier or request.POST.exporter == e.identifier %}open{% endif %}>
<summary class="panel-heading">
<h3 class="panel-title">
{{ e.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</h3>
</summary>
<div id="{{ e.identifier }}">
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='control' %}
<button class="btn btn-primary pull-right flip" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
</div>
</div>
</details>
{% endfor %}
{% endblock %}

View File

@@ -1,19 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete scheduled export" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete scheduled export" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the scheduled export <strong>{{ export }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.orders.export" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

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