Compare commits

..

4 Commits

Author SHA1 Message Date
Richard Schreiber
4e1e082ee3 fix icon alignment 2023-06-05 09:40:56 +02:00
Richard Schreiber
9a57371f9e move seat-info inside panel next to date-info 2023-06-05 09:40:56 +02:00
Richard Schreiber
8bc7045bba add seating-info to panel-title 2023-06-05 09:40:56 +02:00
Richard Schreiber
3c29223e5c Checkout/Add-ons: Do not show products without add-ons 2023-06-05 09:40:56 +02:00
301 changed files with 90735 additions and 132676 deletions

View File

@@ -35,7 +35,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install Dependencies - name: Install Dependencies
run: pip3 install -e ".[dev]" psycopg2-binary run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
- name: Run isort - name: Run isort
run: isort -c . run: isort -c .
working-directory: ./src working-directory: ./src
@@ -55,7 +55,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install Dependencies - name: Install Dependencies
run: pip3 install -e ".[dev]" psycopg2-binary run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
- name: Run flake8 - name: Run flake8
run: flake8 . run: flake8 .
working-directory: ./src working-directory: ./src

View File

@@ -25,14 +25,24 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11"] python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres] database: [sqlite, postgres, mysql]
exclude: exclude:
- database: mysql
python-version: "3.9"
- database: mysql
python-version: "3.11"
- database: sqlite - database: sqlite
python-version: "3.9" python-version: "3.9"
- database: sqlite - database: sqlite
python-version: "3.10" python-version: "3.10"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.10'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
- uses: harmon758/postgresql-action@v1 - uses: harmon758/postgresql-action@v1
with: with:
postgresql version: '11' postgresql version: '11'
@@ -51,9 +61,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install system dependencies - name: Install system dependencies
run: sudo apt update && sudo apt install gettext run: sudo apt update && sudo apt install gettext mariadb-client
- name: Install Python dependencies - name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
- name: Run checks - name: Run checks
run: python manage.py check run: python manage.py check
working-directory: ./src working-directory: ./src

View File

@@ -3,6 +3,7 @@ FROM python:3.11-bullseye
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
build-essential \ build-essential \
libmariadb-dev \
gettext \ gettext \
git \ git \
libffi-dev \ libffi-dev \
@@ -57,7 +58,7 @@ RUN pip3 install -U \
wheel && \ wheel && \
cd /pretix && \ cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \ PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached]" \ -e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \ gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip rm -rf ~/.cache/pip

View File

@@ -154,15 +154,23 @@ Example::
port=3306 port=3306
``backend`` ``backend``
One of ``sqlite3`` and ``postgresql``. One of ``mysql`` (deprecated), ``sqlite3`` and ``postgresql``.
Default: ``sqlite3``. Default: ``sqlite3``.
If you use MySQL, be sure to create your database using
``CREATE DATABASE <dbname> CHARACTER SET utf8;``. Otherwise, Unicode
support will not properly work.
``name`` ``name``
The database's name. Default: ``db.sqlite3``. The database's name. Default: ``db.sqlite3``.
``user``, ``password``, ``host``, ``port`` ``user``, ``password``, ``host``, ``port``
Connection details for the database connection. Empty by default. Connection details for the database connection. Empty by default.
``galera``
(Deprecated) Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`: .. _`config-replica`:
Database replica settings Database replica settings

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_ * `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for * 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 HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 11+ database server * A `PostgreSQL`_ 9.6+ database server
* A `redis`_ 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 We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -321,11 +321,11 @@ workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/ .. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04 .. _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/ .. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/ .. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/ .. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04 .. _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/ .. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall .. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _redis website: https://redis.io/topics/security .. _redis website: https://redis.io/topics/security

View File

@@ -16,11 +16,14 @@ To use pretix, you will need the following things:
* A periodic task runner, e.g. ``cron`` * A periodic task runner, e.g. ``cron``
* **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either * **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either
go for **PostgreSQL**. If you do not provide one, pretix will run on SQLite, which is useful go for **PostgreSQL** or **MySQL/MariaDB**. If you do not provide one, pretix will run on SQLite, which is useful
for evaluation and development purposes. for evaluation and development purposes.
.. warning:: Do not ever use SQLite in production. It will break. .. warning:: Do not ever use SQLite in production. It will break.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix * A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption. faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -21,7 +21,6 @@ Requirements
Please set up the following systems beforehand, we'll not explain them here in detail (but see these links for external Please set up the following systems beforehand, we'll not explain them here in detail (but see these links for external
installation guides): installation guides):
* A python 3.9+ installation
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for * 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 HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 11+ database server * A `PostgreSQL`_ 11+ database server
@@ -324,11 +323,11 @@ Then, proceed like after any plugin installation::
(venv)$ python -m pretix updatestyles (venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker # 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-22-04 .. _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/ .. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/ .. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/ .. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04 .. _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/ .. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall .. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/ .. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -3,11 +3,11 @@
Migrating from MySQL/MariaDB to PostgreSQL Migrating from MySQL/MariaDB to PostgreSQL
========================================== ==========================================
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB has been removed Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB will be removed in
in newer pretix releases. pretix 5.0.
In order to follow this guide, your pretix installation needs to be a version that fully supports MySQL/MariaDB. If you 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 or later, downgrade back to the last 4.x release using ``pip``. 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 .. 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 risk should be low as long as pretix is not running while you do the migration. If you are a pretix Enterprise

View File

@@ -70,11 +70,6 @@ Endpoints
The ``public_url`` field has been added. The ``public_url`` field has been added.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
.. http:get:: /api/v1/organizers/(organizer)/events/ .. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to. Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -146,10 +141,6 @@ Endpoints
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned. :query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned. :query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned. :query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned. :query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and :query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything. ``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.

View File

@@ -111,7 +111,7 @@ Listing available exporters
"input_parameters": [ "input_parameters": [
{ {
"name": "events", "name": "events",
"required": false "required": true
}, },
{ {
"name": "_format", "name": "_format",

View File

@@ -19,7 +19,6 @@ Field Type Description
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
id integer Internal ID of the medium id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``. type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``. identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used. active boolean Whether this medium may be used.
created datetime Date of creation created datetime Date of creation
@@ -68,7 +67,6 @@ Endpoints
"results": [ "results": [
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -125,7 +123,6 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -155,9 +152,6 @@ Endpoints
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes. medium behind the scenes.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -182,7 +176,6 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -242,7 +235,6 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -299,7 +291,6 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",

View File

@@ -63,11 +63,6 @@ last_modified datetime Last modificati
The ``search`` query parameter has been added to filter sub-events by their name or location in any language. The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
Endpoints Endpoints
--------- ---------
@@ -135,10 +130,6 @@ Endpoints
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned. :query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. :query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. :query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. :query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query search: Only return events matching a given search query. :query search: Only return events matching a given search query.
:param organizer: The ``slug`` field of a valid organizer :param organizer: The ``slug`` field of a valid organizer
@@ -467,10 +458,6 @@ Endpoints
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned. :query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. :query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. :query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. :query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel. :query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
:param organizer: The ``slug`` field of a valid organizer :param organizer: The ``slug`` field of a valid organizer

View File

@@ -20,16 +20,11 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
be ignored if custom rules are set.
home_country string Merchant country (required for reverse charge), can be home_country string Merchant country (required for reverse charge), can be
``null`` or empty string ``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
rules keep the gross price constant (default is ``false``) rules keep the gross price constant (default is ``false``)
custom_rules object Dynamic rules specification. Each list element
corresponds to one rule that will be processed in order.
The current version of the schema in use can be found
`here`_.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -37,10 +32,6 @@ custom_rules object Dynamic rules s
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added. The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
.. versionchanged:: 2023.6
The ``custom_rules`` attribute has been added.
Endpoints Endpoints
--------- ---------
@@ -77,7 +68,6 @@ Endpoints
"price_includes_tax": true, "price_includes_tax": true,
"eu_reverse_charge": false, "eu_reverse_charge": false,
"keep_gross_if_rate_changes": false, "keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE" "home_country": "DE"
} }
] ]
@@ -118,7 +108,6 @@ Endpoints
"price_includes_tax": true, "price_includes_tax": true,
"eu_reverse_charge": false, "eu_reverse_charge": false,
"keep_gross_if_rate_changes": false, "keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE" "home_country": "DE"
} }
@@ -167,7 +156,6 @@ Endpoints
"price_includes_tax": true, "price_includes_tax": true,
"eu_reverse_charge": false, "eu_reverse_charge": false,
"keep_gross_if_rate_changes": false, "keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE" "home_country": "DE"
} }
@@ -215,7 +203,6 @@ Endpoints
"price_includes_tax": true, "price_includes_tax": true,
"eu_reverse_charge": false, "eu_reverse_charge": false,
"keep_gross_if_rate_changes": false, "keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE" "home_country": "DE"
} }
@@ -255,5 +242,3 @@ Endpoints
:statuscode 204: no error :statuscode 204: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use. :statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json

View File

@@ -50,10 +50,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.payment.confirmed`` * ``pretix.event.order.payment.confirmed``
* ``pretix.event.order.approved`` * ``pretix.event.order.approved``
* ``pretix.event.order.denied`` * ``pretix.event.order.denied``
* ``pretix.event.orders.waitinglist.added``
* ``pretix.event.orders.waitinglist.changed``
* ``pretix.event.orders.waitinglist.deleted``
* ``pretix.event.orders.waitinglist.voucher_assigned``
* ``pretix.event.checkin`` * ``pretix.event.checkin``
* ``pretix.event.checkin.reverted`` * ``pretix.event.checkin.reverted``
* ``pretix.event.added`` * ``pretix.event.added``

View File

@@ -18,12 +18,12 @@ If you want to add a custom view to the control area of an event, just register
.. code-block:: python .. code-block:: python
from django.urls import re_path from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/', url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.admin_view, name='backend'), views.admin_view, name='backend'),
] ]

View File

@@ -35,12 +35,12 @@ automatically and should be provided by any plugin that provides any view.
A very basic example that provides one view in the admin panel and one view in the frontend A very basic example that provides one view in the admin panel and one view in the frontend
could look like this:: could look like this::
from django.urls import re_path from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/', url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'), views.AdminView.as_view(), name='backend'),
] ]

View File

@@ -1,143 +0,0 @@
ePayBL
======
.. note::
Since ePayBL is only available to german federal, provincial and communal entities, the following page is also
only provided in german. Should you require assistance with ePayBL and do not speak this language, please feel free
reach out to support@pretix.eu.
Einführung
----------
.. note::
Sollten Sie lediglich schnell entscheiden wollen, welcher Kontierungsmodus in den Einstellungen des pretix
ePayBL-plugins gewählt werden soll, so springen Sie direkt zur Sektion :ref:`Kontierungsmodus`.
`ePayBL`_ - das ePayment-System von Bund und Länder - ist das am weitesten verbreitete Zahlungssystem für Bundes-, Länder-
sowie kommunale Aufgabenträger. Während es nur wie eines von vielen anderen Zahlungssystemen scheint, so bietet es
seinen Nutzern besondere Vorteile, wie die automatische Erfassung von Zahlungsbelegen, dem Übertragen von Buchungen in
Haushaltskassen/-systeme sowie die automatische Erfassung von Kontierungen und Steuermerkmalen.
Rein technisch gesehen ist ePayBL hierbei nicht ein eigenständiger Zahlungsdienstleister sondern nur ein eine Komponente
im komplexen System, dass die Zahlungsabwicklung für Kommunen und Behörden ist.
Im folgenden der schematische Aufbau einer Umgebung, in welcher ePayBL zum Einsatz kommt:
.. figure:: img/epaybl_flowchart.png
:class: screenshot
Quelle: Integrationshandbuch ePayBL-Konnektor, DResearch Digital Media Systems GmbH
In diesem Schaubild stellt pretix, bzw. die von Ihnen als Veranstalter angelegten Ticketshops, das Fachverfahren dar.
ePayBL stellt das Bindeglied zwischen den Fachverfahren, Haushaltssystemen und dem eigentlichen Zahlungsdienstleister,
dem sog. ZV-Provider dar. Dieser ZV-Provider ist die Stelle, welche die eigentlichen Kundengelder einzieht und an den
Händler auszahlt. Das Gros der Zahlungsdienstleister unterstützt pretix hierbei auch direkt; sprich: Sollten Sie die
Anbindung an Ihre Haushaltssysteme nicht benötigen, kann eine direkte Anbindung in der Regel ebenso - und dies bei meist
vermindertem Aufwand - vorgenommen werden.
In der Vergangenheit zeigte sich jedoch schnell, dass nicht jeder IT-Dienstleister immer sofort die neueste Version von
ePayBL seinen Nutzern angeboten hat. Die Gründe hierfür sind mannigfaltig: Von fest vorgegebenen Update-Zyklen bis hin
zu Systeme mit speziellen Anpassungen, kann leider nicht davon ausgegangen werden, dass alle ePayBL-Systeme exakt gleich
ansprechbar sind - auch wenn es sich dabei eigentlich um einen standardisierten Dienst handelt.
Aus diesem Grund gibt es mit dem ePayBL-Konnektor eine weitere Abstraktionsschicht welche optional zwischen den
Fachverfahren und dem ePayBL-Server sitzt. Dieser Konnektor wird so gepflegt, dass er zum einen eine dauerhaft
gleichartige Schnittstelle den Fachverfahren bietet aber gleichzeitig auch mit jeder Version des ePayBL-Servers
kommunizieren kann - egal wie neu oder alt, wie regulär oder angepasst diese ist.
Im Grunde müsste daher eigentlich immer gesagt werden, dass pretix eine Anbindung an den ePayBL-Konnektor bietet; nicht
an "ePayBL" oder den "ePayBL-Server". Diese Unterscheidung kann bei der Ersteinrichtung und Anforderung von Zugangsdaten
von Relevanz sein. Da in der Praxis jedoch beide Begriffe gleichbedeutend genutzt werden, wird im Folgenden auch nur von
einer ePayBL-Anbindung die Rede sein - auch wenn explizit der Konnektor gemeint ist.
.. _`Kontierungsmodus`:
Kontierungsmodus
----------------
ePayBL ist ein Produkt, welches für die Abwicklung von Online-Zahlungsvorgängen in der Verwaltung geschaffen wurde. Ein
Umfeld, in dem klar definiert ist, was ein Kunde gerade bezahlt und wohin das Geld genau fließt. Diese Annahmen lassen
sich in einem Ticketshop wie pretix jedoch nur teilweise genauso abbilden.
Die ePayBL-Integration für pretix bietet daher zwei unterschiedliche Modi an, wie Buchungen erfasst und an ePayBL und
damit auch an die dahinterliegenden Haushaltssysteme gemeldet werden können.
Kontierung pro Position/Artikel
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus versucht den klassischen, behördentypischen ePayBL-Zahlungsvorgang abzubilden: Jede einzelne Position, die
ein Kunde in den Warenkorb legt, wird auch genauso 1:1 an ePayBL und die Hintergrundsysteme übermittelt.
Hierbei muss zwingend auch für jede Position ein Kennzeichen für Haushaltsstelle und Objektnummer, sowie optional ein
Kontierungsobjekt (``HREF``; bspw. ``stsl=Steuerschlüssel;psp=gsb:Geschäftsbereich,auft:Innenauftrag,kst:Kostenstelle;``
) übermittelt werden.
Diese Daten sind vom Veranstalter entsprechend für jeden in der Veranstaltung angelegten Artikel innerhalb des Tabs
"Zusätzliche Einstellungen" der Produkteinstellungen zu hinterlegen.
Während diese Einstellung eine größtmögliche Menge an Kontierungsdaten überträgt und auch ein separates Verbuchen von
Leistungen auf unterschiedliche Haushaltsstellen erlaubt, so hat diese Option auch einen großen Nachteil: Der Kunde kann
nur eine Zahlung für seine Bestellung leisten.
Während sich dies nicht nach einem großen Problem anhört, so kann dies beim Kunden zu Frust führen. pretix bietet die
Option an, dass ein Veranstalter eine Bestellung jederzeit verändern kann: Ändern von Preisen von Positionen in einer
aufgegebenen Bestellung, Zubuchen und Entfernen von Bestellpositionen, etc. Hat der Kunde seine ursprüngliche Bestellung
jedoch schon bezahlt, kann pretix nicht mehr die komplette Bestellung mit den passenden Kontierungen übertragen - es
müsste nur ein Differenz-Abbild zwischen Ursprungsbestellung und aktueller Bestellung übertragen werden. Aber auch wenn
eine "Nachmeldung" möglich wäre, so wäre ein konkretes Auflösen für was jetzt genau gezahlt wird, nicht mehr möglich.
Daher gilt bei der Nutzung der Kontierung pro Position/Artikel: Der Kunde kann nur eine (erfolgreiche) Zahlung auf seine
Bestellung leisten.
Eine weitere Einschränkung dieses Modus ist, dass aktuell keine Gebühren-Positionen (Versandkosten, Zahlungs-, Storno-
oder Servicegebühren) in diesem Modus übertragen werden können. Bitte wenden Sie sich an uns, wenn Sie diese
Funktionalität benötigen.
Kontierung pro Zahlvorgang
^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus verabschiedet sich vom behördlichen "Jede Position gehört genau zu einem Haushaltskonto und muss genau
zugeordnet werden". Stattdessen werden alle Bestellpositionen - inklusive eventuell definierter Gebühren - vermengt und
nur als ein großer Warenkorb, genauer gesagt: eine einzige Position an ePayBL sowie die Hintergrundsysteme gemeldet.
Während im "pro Postion/Artikel"-Modus jeder Artikel einzeln übermittelt wird und damit auch korrekt pro Artikel der
jeweilige Brutto- und Nettopreis, sowie der anfallende Steuerbetrag und ein Steuerkennzeichen (mit Hilfe des optionalen
``HREF``-Attributs) übermittelt werden, ist dies im "pro Zahlvorgang"-Modus nicht möglich.
Stattdessen übermittelt pretix nur einen Betrag für den gesamten Warenkorb: Bruttopreis == Nettopreis. Der Steuerbetrag
wird hierbei als 0 übermittelt.
Die Angabe einer Haushaltsstelle und Objektnummer, sowie optional der ``HREF``-Kontierungsinformationen ist jedoch
weiterhin notwendig - allerdings nicht mehr individuell für jeden Artikel/jede Position sondern nur für die gesamte
Bestellung. Diese Daten sind direkt in den ePayBL-Einstellungen der Veranstaltung unter Einstellungen -> Zahlung ->
ePayBL vorzunehmen
In der Praxis bedeutet dies, dass in einem angeschlossenen Haushaltssystem nicht nachvollzogen kann, welche Positionen
konkret erworben und bezahlt wurden - stattdessen kann nur der Fakt, dass etwas verkauft wurde erfasst werden.
Je nach Aufbau und Vorgaben der Finanzbuchhaltung kann dies jedoch ausreichend sein - wenn bspw. eine Ferienfahrt
angeboten wird und seitens der Haushaltssysteme nicht erfasst werden muss, wie viel vom Gesamtbetrag einer Bestellung
auf die Ferienfahrt an sich, auf einen Zubringerbus und einen Satz Bettwäsche entfallen ist, sondern (vereinfacht
gesagt) es ausreichend ist, dass "Eine Summe X für die Haushaltsstelle/Objektnummer geflossen ist".
Dieser Modus der Kontierung bietet Ihnen auch als Vorteil gegenüber dem vorhergehenden an, dass die Bestellungen der
Kunden jederzeit erweitert und verändert werden können - auch wenn die Ursprungsbestellung schon bezahlt wurde und nur
noch eine Differenz gezahlt wird.
Einschränkungen
---------------
Zum aktuellen Zeitpunkt erlaubt die pretix-Anbindung an ePayBL nicht das durchführen von Erstattungen von bereits
geleisteten Zahlungen. Der Prozess hierfür unterscheidet sich von Behörde zu Behörde und muss daher händisch
durchgeführt werden.
.. _ePayBL: https://www.epaybl.de/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -18,7 +18,6 @@ If you want to **create** a plugin, please go to the
campaigns campaigns
certificates certificates
digital digital
epaybl
exhibitors exhibitors
shipping shipping
imported_secrets imported_secrets

View File

@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Framework :: Django :: 4.1", "Framework :: Django :: 3.2",
] ]
dependencies = [ dependencies = [
@@ -36,7 +36,7 @@ dependencies = [
"css-inline==0.8.*", "css-inline==0.8.*",
"defusedcsv>=1.1.0", "defusedcsv>=1.1.0",
"dj-static", "dj-static",
"Django==4.1.*", "Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*", "django-bootstrap3==23.1.*",
"django-compressor==4.3.*", "django-compressor==4.3.*",
"django-countries==7.5.*", "django-countries==7.5.*",
@@ -49,6 +49,7 @@ dependencies = [
"django-libsass==0.9", "django-libsass==0.9",
"django-localflavor==4.0", "django-localflavor==4.0",
"django-markup", "django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*", "django-oauth-toolkit==2.2.*",
"django-otp==1.2.*", "django-otp==1.2.*",
"django-phonenumber-field==7.1.*", "django-phonenumber-field==7.1.*",
@@ -73,10 +74,9 @@ dependencies = [
"packaging", "packaging",
"paypalrestsdk==1.13.*", "paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.7.*", "PyJWT==2.6.*",
"phonenumberslite==8.13.*", "phonenumberslite==8.13.*",
"Pillow==9.5.*", "Pillow==9.5.*",
"pretix-plugin-build",
"protobuf==4.23.*", "protobuf==4.23.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
@@ -87,12 +87,11 @@ dependencies = [
"python-dateutil==2.8.*", "python-dateutil==2.8.*",
"python-u2flib-server==4.*", "python-u2flib-server==4.*",
"pytz", "pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca", "pyuca",
"qrcode==7.4.*", "qrcode==7.4.*",
"redis==4.5.*,>=4.5.4", "redis==4.5.*,>=4.5.4",
"reportlab==4.0.*", "reportlab==4.0.*",
"requests==2.31.*", "requests==2.30.*",
"sentry-sdk==1.15.*", "sentry-sdk==1.15.*",
"sepaxml==2.6.*", "sepaxml==2.6.*",
"slimit", "slimit",
@@ -109,6 +108,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
memcached = ["pylibmc"] memcached = ["pylibmc"]
mysql = ["mysqlclient"]
dev = [ dev = [
"coverage", "coverage",
"coveralls", "coveralls",
@@ -125,7 +125,7 @@ dev = [
"pytest-mock==3.10.*", "pytest-mock==3.10.*",
"pytest-rerunfailures==11.*", "pytest-rerunfailures==11.*",
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.3.*", "pytest-xdist==3.2.*",
"pytest==7.3.*", "pytest==7.3.*",
"responses", "responses",
] ]

View File

@@ -29,6 +29,7 @@ sys.path.append(str(Path.cwd() / 'src'))
def _CustomBuild(*args, **kwargs): def _CustomBuild(*args, **kwargs):
print(sys.path)
from pretix._build import CustomBuild from pretix._build import CustomBuild
return CustomBuild(*args, **kwargs) return CustomBuild(*args, **kwargs)

View File

@@ -30,6 +30,7 @@ from django.utils.translation import gettext_lazy as _ # NOQA
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -67,7 +68,6 @@ INSTALLED_APPS = [
'oauth2_provider', 'oauth2_provider',
'phonenumber_field', 'phonenumber_field',
'statici18n', 'statici18n',
'django.forms', # after pretix.base for overrides
] ]
FORMAT_MODULE_PATH = [ FORMAT_MODULE_PATH = [
@@ -180,8 +180,6 @@ TEMPLATES = [
}, },
] ]
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist') STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (

View File

@@ -45,10 +45,6 @@ def npm_install():
class CustomBuild(build): class CustomBuild(build):
def run(self): def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ: if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching return # this is a hack to allow calling this file early in our docker build to make use of caching
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
@@ -72,10 +68,6 @@ class CustomBuild(build):
class CustomBuildExt(build_ext): class CustomBuildExt(build_ext):
def run(self): def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build_ext.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ: if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching return # this is a hack to allow calling this file early in our docker build to make use of caching
npm_install() npm_install()

View File

@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import json
from rest_framework import serializers from rest_framework import serializers
@@ -48,16 +46,3 @@ class AsymmetricField(serializers.Field):
def run_validation(self, data=serializers.empty): def run_validation(self, data=serializers.empty):
return self.write.run_validation(data) return self.write.run_validation(data)
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value

View File

@@ -46,7 +46,6 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
@@ -54,7 +53,6 @@ from pretix.base.models.event import SubEvent
from pretix.base.models.items import ( from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation, ItemMetaProperty, SubEventItem, SubEventItemVariation,
) )
from pretix.base.models.tax import CustomRulesValidator
from pretix.base.services.seating import ( from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change, SeatProtected, generate_seats, validate_plan_change,
) )
@@ -652,16 +650,9 @@ class SubEventSerializer(I18nAwareModelSerializer):
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer): class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
custom_rules = CompatibleJSONField(
validators=[CustomRulesValidator()],
required=False,
allow_null=True,
)
class Meta: class Meta:
model = TaxRule model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
'keep_gross_if_rate_changes', 'custom_rules')
class EventSettingsSerializer(SettingsSerializer): class EventSettingsSerializer(SettingsSerializer):

View File

@@ -93,7 +93,7 @@ class JobRunSerializer(serializers.Serializer):
if events is not None and not isinstance(ex, OrganizerLevelExportMixin): if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField( self.fields["events"] = serializers.SlugRelatedField(
queryset=events, queryset=events,
required=False, required=True,
allow_empty=False, allow_empty=False,
slug_field='slug', slug_field='slug',
many=True many=True
@@ -156,9 +156,8 @@ class JobRunSerializer(serializers.Serializer):
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, QueryDict): if isinstance(data, QueryDict):
data = data.copy() data = data.copy()
for k, v in self.fields.items(): for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data and k != "events": if isinstance(v, serializers.ManyRelatedField) and k not in data:
data[k] = [] data[k] = []
for fk in self.fields.keys(): for fk in self.fields.keys():

View File

@@ -60,8 +60,6 @@ class NestedGiftCardSerializer(GiftCardSerializer):
class ReusableMediaSerializer(I18nAwareModelSerializer): class ReusableMediaSerializer(I18nAwareModelSerializer):
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -113,7 +111,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
model = ReusableMedium model = ReusableMedium
fields = ( fields = (
'id', 'id',
'organizer',
'created', 'created',
'updated', 'updated',
'type', 'type',

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import json
import logging import logging
import os import os
from collections import Counter, defaultdict from collections import Counter, defaultdict
@@ -38,7 +39,6 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import ( from pretix.api.serializers.item import (
@@ -535,7 +535,6 @@ class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2 # TODO: Remove after pretix 2.2
def to_representation(self, instance: Order): def to_representation(self, instance: Order):
t = None t = None
if instance.pk:
for p in instance.payments.all(): for p in instance.payments.all():
t = p.provider t = p.provider
return t return t
@@ -545,10 +544,10 @@ class OrderPaymentDateField(serializers.DateField):
# TODO: Remove after pretix 2.2 # TODO: Remove after pretix 2.2
def to_representation(self, instance: Order): def to_representation(self, instance: Order):
t = None t = None
if instance.pk:
for p in instance.payments.all(): for p in instance.payments.all():
t = p.payment_date or t t = p.payment_date or t
if t: if t:
return super().to_representation(t.date()) return super().to_representation(t.date())
@@ -896,6 +895,19 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
return data return data
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value
class WrappedList: class WrappedList:
def __init__(self, data): def __init__(self, data):
self._data = data self._data = data
@@ -1351,7 +1363,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answers.append(answ) answers.append(answ)
pos.answers = answers pos.answers = answers
pos.pseudonymization_id = "PREVIEW" pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos_map[pos.positionid] = pos pos_map[pos.positionid] = pos
else: else:
if pos.voucher: if pos.voucher:
@@ -1448,8 +1459,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if simulate: if simulate:
order.fees = fees order.fees = fees
order.positions = pos_map.values() order.positions = pos_map.values()
order.payments = []
order.refunds = []
return order # ignore payments return order # ignore payments
else: else:
order.save(update_fields=['total']) order.save(update_fields=['total'])

View File

@@ -36,9 +36,9 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region from pretix.base.i18n import get_language_without_region
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction, Customer, Device, GiftCard, GiftCardTransaction, Membership,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
SeatingPlan, Team, TeamAPIToken, TeamInvite, User, Team, TeamAPIToken, TeamInvite, User,
) )
from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
@@ -183,11 +183,8 @@ class GiftCardSerializer(I18nAwareModelSerializer):
qs = GiftCard.objects.filter( qs = GiftCard.objects.filter(
secret=s secret=s
).filter( ).filter(
Q(issuer=self.context["organizer"]) | Q(issuer=self.context["organizer"]) | Q(
Q(issuer__in=GiftCardAcceptance.objects.filter( issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
acceptor=self.context["organizer"],
active=True,
).values_list('issuer', flat=True))
) )
if self.instance: if self.instance:
qs = qs.exclude(pk=self.instance.pk) qs = qs.exclude(pk=self.instance.pk)

View File

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

View File

@@ -396,7 +396,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce, def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported, untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False, simulate=False): source_type='barcode', legacy_url_support=False):
if not checkinlists: if not checkinlists:
raise ValidationError('No check-in list passed.') raise ValidationError('No check-in list passed.')
@@ -433,8 +433,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
) )
raw_barcode_for_checkin = None raw_barcode_for_checkin = None
from_revoked_secret = False from_revoked_secret = False
if simulate:
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or # 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
# parent secret # parent secret
@@ -474,7 +472,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
revoked_matches = list( revoked_matches = list(
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode)) RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0: if len(revoked_matches) == 0:
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={ checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime, 'datetime': datetime,
'type': checkin_type, 'type': checkin_type,
@@ -495,7 +492,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
except: except:
pass pass
if not simulate:
Checkin.objects.create( Checkin.objects.create(
position=None, position=None,
successful=False, successful=False,
@@ -543,7 +539,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
from_revoked_secret = True from_revoked_secret = True
else: else:
op = revoked_matches[0].position op = revoked_matches[0].position
if not simulate:
op.order.log_action('pretix.event.checkin.revoked', data={ op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime, 'datetime': datetime,
'type': checkin_type, 'type': checkin_type,
@@ -593,7 +588,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# We choose the first match (regardless of product) for the logging since it's most likely to be the # We choose the first match (regardless of product) for the logging since it's most likely to be the
# base product according to our order_by above. # base product according to our order_by above.
op = op_candidates[0] op = op_candidates[0]
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={ op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id, 'position': op.id,
'positionid': op.positionid, 'positionid': op.positionid,
@@ -658,7 +652,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_barcode=raw_barcode_for_checkin, raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type, raw_source_type=source_type,
from_revoked_secret=from_revoked_secret, from_revoked_secret=from_revoked_secret,
simulate=simulate,
) )
except RequiredQuestionsError as e: except RequiredQuestionsError as e:
return Response({ return Response({
@@ -671,7 +664,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data, 'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400) }, status=400)
except CheckInError as e: except CheckInError as e:
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={ op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id, 'position': op.id,
'positionid': op.positionid, 'positionid': op.positionid,

View File

@@ -71,8 +71,6 @@ with scopes_disabled():
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs') sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs') search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta: class Meta:
model = Event model = Event
@@ -338,8 +336,6 @@ with scopes_disabled():
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs') sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs') search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta: class Meta:
model = SubEvent model = SubEvent

View File

@@ -133,12 +133,7 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
def exporters(self): def exporters(self):
exporters = [] exporters = []
responses = register_data_exporters.send(self.request.event) responses = register_data_exporters.send(self.request.event)
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response] for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex) ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex) exporters.append(ex)
return exporters return exporters
@@ -171,7 +166,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
if ( if (
not isinstance(ex, OrganizerLevelExportMixin) or not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request) perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None) )
] ]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)): for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events) ex._serializer = JobRunSerializer(exporter=ex, events=events)

View File

@@ -39,8 +39,7 @@ from pretix.api.serializers.media import (
) )
from pretix.base.media import MEDIA_TYPES from pretix.base.media import MEDIA_TYPES
from pretix.base.models import ( from pretix.base.models import (
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition, Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
ReusableMedium,
) )
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
@@ -135,22 +134,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
) )
s = self.get_serializer(m) s = self.get_serializer(m)
return Response({"result": s.data}) return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
try:
with scopes_disabled():
m = ReusableMedium.objects.get(
organizer__in=GiftCardAcceptance.objects.filter(
acceptor=request.organizer,
active=True,
reusable_media=True,
).values_list('issuer', flat=True),
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist: except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"]) mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt: if mt:

View File

@@ -23,9 +23,9 @@ import datetime
import mimetypes import mimetypes
import os import os
from decimal import Decimal from decimal import Decimal
from zoneinfo import ZoneInfo
import django_filters import django_filters
import pytz
from django.db import transaction from django.db import transaction
from django.db.models import ( from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects, Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
@@ -612,7 +612,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
tz = ZoneInfo(self.request.event.settings.timezone) tz = pytz.timezone(self.request.event.settings.timezone)
new_date = make_aware(datetime.datetime.combine( new_date = make_aware(datetime.datetime.combine(
new_date, new_date,
datetime.time(hour=23, minute=59, second=59) datetime.time(hour=23, minute=59, second=59)
@@ -661,16 +661,7 @@ class OrderViewSet(viewsets.ModelViewSet):
with language(order.locale, self.request.event.settings.region): with language(order.locale, self.request.event.settings.region):
payment = order.payments.last() payment = order.payments.last()
# OrderCreateSerializer creates at most one payment
if payment and payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
order.log_action(
'pretix.event.order.payment.confirmed', {
'local_id': payment.local_id,
'provider': payment.provider,
},
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order) order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID: if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order) order_paid.send(self.request.event, order=order)

View File

@@ -189,19 +189,6 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
return d return d
class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'waitinglistentry': logentry.object_id,
'action': logentry.action_type,
}
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events") @receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs): def register_default_webhook_events(sender, **kwargs):
return ( return (
@@ -334,22 +321,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.testmode.deactivated', 'pretix.event.testmode.deactivated',
_('Test-Mode of shop has been deactivated'), _('Test-Mode of shop has been deactivated'),
), ),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.added',
_('Waiting list entry added'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.changed',
_('Waiting list entry changed'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.deleted',
_('Waiting list entry deleted'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
) )

View File

@@ -37,8 +37,8 @@ import tempfile
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from decimal import Decimal from decimal import Decimal
from typing import Optional, Tuple from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import pytz
from defusedcsv import csv from defusedcsv import csv
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@@ -68,7 +68,7 @@ class BaseExporter:
self.events = event self.events = event
self.event = None self.event = None
e = self.events.first() e = self.events.first()
self.timezone = e.timezone if e else ZoneInfo(settings.TIME_ZONE) self.timezone = e.timezone if e else pytz.timezone(settings.TIME_ZONE)
else: else:
self.events = Event.objects.filter(pk=event.pk) self.events = Event.objects.filter(pk=event.pk)
self.timezone = event.timezone self.timezone = event.timezone
@@ -157,13 +157,6 @@ class BaseExporter:
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
def available_for_user(self, user) -> bool:
"""
Allows to do additional checks whether an exporter is available based on the user who calls it. Note that
``user`` may be ``None`` e.g. during API usage.
"""
return True
class OrganizerLevelExportMixin: class OrganizerLevelExportMixin:
@property @property

View File

@@ -34,8 +34,8 @@
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
from zoneinfo import ZoneInfo
import pytz
from django import forms from django import forms
from django.db.models import ( from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef, Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
@@ -326,7 +326,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count()) yield self.ProgressSetTotal(total=qs.count())
for order in qs.order_by('datetime').iterator(): for order in qs.order_by('datetime').iterator():
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone) tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
row = [ row = [
self.event_object_cache[order.event_id].slug, self.event_object_cache[order.event_id].slug,
@@ -459,7 +459,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count()) yield self.ProgressSetTotal(total=qs.count())
for op in qs.order_by('order__datetime').iterator(): for op in qs.order_by('order__datetime').iterator():
order = op.order order = op.order
tz = ZoneInfo(order.event.settings.timezone) tz = pytz.timezone(order.event.settings.timezone)
row = [ row = [
self.event_object_cache[order.event_id].slug, self.event_object_cache[order.event_id].slug,
order.code, order.code,
@@ -631,7 +631,7 @@ class OrderListExporter(MultiSheetListExporter):
for op in ops: for op in ops:
order = op.order order = op.order
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone) tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
row = [ row = [
self.event_object_cache[order.event_id].slug, self.event_object_cache[order.event_id].slug,
order.code, order.code,
@@ -850,8 +850,6 @@ class TransactionListExporter(ListExporter):
_('Tax rule ID'), _('Tax rule ID'),
_('Tax rule'), _('Tax rule'),
_('Tax value'), _('Tax value'),
_('Gross total'),
_('Tax total'),
] ]
if form_data.get('_format') == 'xlsx': if form_data.get('_format') == 'xlsx':
@@ -903,8 +901,6 @@ class TransactionListExporter(ListExporter):
t.tax_rule_id or '', t.tax_rule_id or '',
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '', str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
t.tax_value, t.tax_value,
t.price * t.count,
t.tax_value * t.count,
] ]
if form_data.get('_format') == 'xlsx': if form_data.get('_format') == 'xlsx':
@@ -1028,7 +1024,7 @@ class PaymentListExporter(ListExporter):
yield self.ProgressSetTotal(total=len(objs)) yield self.ProgressSetTotal(total=len(objs))
for obj in objs: for obj in objs:
tz = ZoneInfo(obj.order.event.settings.timezone) tz = pytz.timezone(obj.order.event.settings.timezone)
if isinstance(obj, OrderPayment) and obj.payment_date: if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d') d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date: elif isinstance(obj, OrderRefund) and obj.execution_date:
@@ -1147,7 +1143,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
def iterate_list(self, form_data): def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter( qs = GiftCardTransaction.objects.filter(
card__issuer=self.organizer, card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event', 'acceptor') ).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_range'): 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) dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
@@ -1163,7 +1159,6 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
_('Amount'), _('Amount'),
_('Currency'), _('Currency'),
_('Order'), _('Order'),
_('Organizer'),
] ]
yield headers yield headers
@@ -1175,7 +1170,6 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
obj.value, obj.value,
obj.card.currency, obj.card.currency,
obj.order.full_code if obj.order else None, obj.order.full_code if obj.order else None,
str(obj.acceptor or ""),
] ]
yield row yield row
@@ -1209,7 +1203,7 @@ class GiftcardRedemptionListExporter(ListExporter):
yield headers yield headers
for obj in objs: for obj in objs:
tz = ZoneInfo(obj.order.event.settings.timezone) tz = pytz.timezone(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card')) gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [ row = [
obj.order.event.slug, obj.order.event.slug,

View File

@@ -20,8 +20,8 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from collections import OrderedDict from collections import OrderedDict
from zoneinfo import ZoneInfo
import pytz
from django import forms from django import forms
from django.db.models import F, Q from django.db.models import F, Q
from django.dispatch import receiver from django.dispatch import receiver
@@ -137,7 +137,7 @@ class WaitingListExporter(ListExporter):
# which event should be used to output dates in columns "Start date" and "End date" # which event should be used to output dates in columns "Start date" and "End date"
event_for_date_columns = entry.subevent if entry.subevent else entry.event event_for_date_columns = entry.subevent if entry.subevent else entry.event
tz = ZoneInfo(entry.event.settings.timezone) tz = pytz.timezone(entry.event.settings.timezone)
datetime_format = '%Y-%m-%d %H:%M:%S' datetime_format = '%Y-%m-%d %H:%M:%S'
row = [ row = [

View File

@@ -167,7 +167,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
class PrefixForm(forms.Form): class PrefixForm(forms.Form):
prefix = forms.CharField(widget=forms.HiddenInput) prefix = forms.CharField(widget=forms.HiddenInput)
template_name = "django/forms/table.html"
class SafeSessionWizardView(SessionWizardView): class SafeSessionWizardView(SessionWizardView):

View File

@@ -38,10 +38,10 @@ import logging
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
from zoneinfo import ZoneInfo
import dateutil.parser import dateutil.parser
import pycountry import pycountry
import pytz
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -61,7 +61,6 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries from django_countries import countries
from django_countries.fields import Country, CountryField from django_countries.fields import Country, CountryField
from geoip2.errors import AddressNotFoundError
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget from phonenumber_field.widgets import PhoneNumberPrefixWidget
@@ -357,12 +356,9 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event): def guess_country_from_request(request, event):
if settings.HAS_GEOIP: if settings.HAS_GEOIP:
g = GeoIP2() g = GeoIP2()
try:
res = g.country(get_client_ip(request)) res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2: if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code']) return Country(res['country_code'])
except AddressNotFoundError:
pass
return guess_country(event) return guess_country(event)
@@ -737,7 +733,7 @@ class BaseQuestionsForm(forms.Form):
initial = answers[0] initial = answers[0]
else: else:
initial = None initial = None
tz = ZoneInfo(event.settings.timezone) tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text) help_text = rich_text(q.help_text)
label = escape(q.question) # django-bootstrap3 calls mark_safe label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional required = q.required and not self.all_optional

View File

@@ -1,63 +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 bootstrap3.renderers import (
FieldRenderer as BaseFieldRenderer,
InlineFieldRenderer as BaseInlineFieldRenderer,
)
from django.forms import (
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
SelectDateWidget,
)
class FieldRenderer(BaseFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
class InlineFieldRenderer(BaseInlineFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html

View File

@@ -24,7 +24,7 @@ Django, for theoretically very valid reasons, creates migrations for *every sing
we change on a model. Even the `help_text`! This makes sense, as we don't know if any we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema. database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL and SQLite and we can be pretty However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case, certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win! *and* allow for a cleaner git history. Win-win!

View File

@@ -22,7 +22,7 @@
import json import json
import sys import sys
import pytz_deprecation_shim import pytz
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.timezone import override from django.utils.timezone import override
from django_scopes import scope from django_scopes import scope
@@ -60,7 +60,7 @@ class Command(BaseCommand):
sys.exit(1) sys.exit(1)
locale = options.get("locale", None) locale = options.get("locale", None)
timezone = pytz_deprecation_shim.timezone(options['timezone']) if options.get('timezone') else None timezone = pytz.timezone(options['timezone']) if options.get('timezone') else None
with scope(organizer=o): with scope(organizer=o):
if options['event_slug']: if options['event_slug']:

View File

@@ -21,8 +21,8 @@
# #
from collections import OrderedDict from collections import OrderedDict
from urllib.parse import urlsplit from urllib.parse import urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytz
from django.conf import settings from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware from django.middleware.common import CommonMiddleware
@@ -98,9 +98,9 @@ class LocaleMiddleware(MiddlewareMixin):
tzname = request.user.timezone tzname = request.user.timezone
if tzname: if tzname:
try: try:
timezone.activate(ZoneInfo(tzname)) timezone.activate(pytz.timezone(tzname))
request.timezone = tzname request.timezone = tzname
except ZoneInfoNotFoundError: except pytz.UnknownTimeZoneError:
pass pass
else: else:
timezone.deactivate() timezone.deactivate()

View File

@@ -2,8 +2,6 @@
# Generated by Django 1.10.4 on 2017-02-03 14:21 # Generated by Django 1.10.4 on 2017-02-03 14:21
from __future__ import unicode_literals from __future__ import unicode_literals
from zoneinfo import ZoneInfo
import django.core.validators import django.core.validators
import django.db.migrations.operations.special import django.db.migrations.operations.special
import django.db.models.deletion import django.db.models.deletion
@@ -28,7 +26,7 @@ def forwards42(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value') for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
} }
for order in Order.objects.all(): for order in Order.objects.all():
tz = ZoneInfo(etz.get(order.event_id, 'UTC')) tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59) order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save() order.save()

View File

@@ -2,9 +2,9 @@
# Generated by Django 1.10.2 on 2016-10-19 17:57 # Generated by Django 1.10.2 on 2016-10-19 17:57
from __future__ import unicode_literals from __future__ import unicode_literals
from zoneinfo import ZoneInfo import pytz
from django.db import migrations from django.db import migrations
from django.utils import timezone
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
@@ -15,7 +15,7 @@ def forwards(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value') for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
} }
for order in Order.objects.all(): for order in Order.objects.all():
tz = ZoneInfo(etz.get(order.event_id, 'UTC')) tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59) order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save() order.save()

View File

@@ -3,6 +3,7 @@
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models from django.db import migrations, models
from django_mysql.checks import mysql_connections
def set_attendee_name_parts(apps, schema_editor): def set_attendee_name_parts(apps, schema_editor):
@@ -23,12 +24,40 @@ def set_attendee_name_parts(apps, schema_editor):
ia.save(update_fields=['name_parts']) ia.save(update_fields=['name_parts'])
def check_mysqlversion(apps, schema_editor):
errors = []
any_conn_works = False
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if hasattr(conn, 'mysql_is_mariadb') and conn.mysql_is_mariadb and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:
found = 'MariaDB ' + '.'.join(str(v) for v in conn.mysql_version)
elif hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (5, 7):
any_conn_works = True
else:
found = 'MySQL ' + '.'.join(str(v) for v in conn.mysql_version)
if conns and not any_conn_works:
raise ImproperlyConfigured(
'As of pretix 2.2, you need MySQL 5.7+ or MariaDB 10.2.7+ to run pretix. However, we detected a '
'database connection to {}'.format(found)
)
return errors
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('pretixbase', '0101_auto_20181025_2255'), ('pretixbase', '0101_auto_20181025_2255'),
] ]
operations = [ operations = [
migrations.RunPython(
check_mysqlversion, migrations.RunPython.noop
),
migrations.RenameField( migrations.RenameField(
model_name='cartposition', model_name='cartposition',
old_name='attendee_name', old_name='attendee_name',

View File

@@ -1,7 +1,8 @@
# Generated by Django 3.2.4 on 2021-09-30 10:25 # Generated by Django 3.2.4 on 2021-09-30 10:25
from datetime import datetime, timezone from datetime import datetime
from django.db import migrations, models from django.db import migrations, models
from pytz import UTC
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -14,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='invoice', model_name='invoice',
name='sent_to_customer', name='sent_to_customer',
field=models.DateTimeField(blank=True, null=True, default=datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc)), field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
preserve_default=False, preserve_default=False,
), ),
] ]

View File

@@ -50,6 +50,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
'unique_together': {('event', 'secret')}, 'unique_together': {('event', 'secret')},
} } if 'mysql' not in settings.DATABASES['default']['ENGINE'] else {}
), ),
] ]

View File

@@ -1,38 +0,0 @@
# Generated by Django 3.2.18 on 2023-05-12 10:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0241_itemmetaproperties_required_values'),
]
operations = [
migrations.RenameField(
model_name='giftcardacceptance',
old_name='collector',
new_name='acceptor',
),
migrations.AddField(
model_name='giftcardacceptance',
name='active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='giftcardacceptance',
name='reusable_media',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='giftcardacceptance',
name='issuer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_card_acceptor_acceptance', to='pretixbase.organizer'),
),
migrations.AlterUniqueTogether(
name='giftcardacceptance',
unique_together={('issuer', 'acceptor')},
),
]

View File

@@ -121,23 +121,14 @@ class Customer(LoggedModel):
if self.email: if self.email:
self.email = self.email.lower() self.email = self.email.lower()
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']: if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields']) kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
if not self.identifier: if not self.identifier:
self.assign_identifier() self.assign_identifier()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
if self.name_parts: if self.name_parts:
name = self.name self.name_cached = self.name
if self.name_cached != name:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else: else:
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = "" self.name_cached = ""
self.name_parts = {} self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs) super().save(**kwargs)
def anonymize(self): def anonymize(self):

View File

@@ -98,8 +98,6 @@ class Gate(LoggedModel):
if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists(): if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists():
self.identifier = code self.identifier = code
break break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@@ -175,8 +173,6 @@ class Device(LoggedModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.device_id: if not self.device_id:
self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1 self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
def permission_set(self) -> set: def permission_set(self) -> set:

View File

@@ -40,9 +40,8 @@ from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from operator import attrgetter from operator import attrgetter
from urllib.parse import urljoin from urllib.parse import urljoin
from zoneinfo import ZoneInfo
import pytz_deprecation_shim import pytz
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@@ -215,7 +214,7 @@ class EventMixin:
@property @property
def timezone(self): def timezone(self):
return pytz_deprecation_shim.timezone(self.settings.timezone) return pytz.timezone(self.settings.timezone)
@property @property
def effective_presale_end(self): def effective_presale_end(self):
@@ -774,7 +773,7 @@ class Event(EventMixin, LoggedModel):
""" """
The last datetime of payments for this event. The last datetime of payments for this event.
""" """
tz = ZoneInfo(self.settings.timezone) tz = pytz.timezone(self.settings.timezone)
return make_aware(datetime.combine( return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(), self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
time(hour=23, minute=59, second=59) time(hour=23, minute=59, second=59)

View File

@@ -19,11 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import zoneinfo
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz
from dateutil.rrule import rrulestr from dateutil.rrule import rrulestr
from dateutil.tz import datetime_exists
from django.conf import settings from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.db import models
@@ -109,9 +108,12 @@ class AbstractScheduledExport(LoggedModel):
self.schedule_next_run = None self.schedule_next_run = None
return return
try:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz) self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
if not datetime_exists(self.schedule_next_run): except pytz.exceptions.AmbiguousTimeError:
self.schedule_next_run += timedelta(hours=1) 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): class ScheduledEventExport(AbstractScheduledExport):
@@ -134,4 +136,4 @@ class ScheduledOrganizerExport(AbstractScheduledExport):
@property @property
def tz(self): def tz(self):
return zoneinfo.ZoneInfo(self.timezone) return pytz.timezone(self.timezone)

View File

@@ -46,19 +46,14 @@ def gen_giftcard_secret(length=8):
class GiftCardAcceptance(models.Model): class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey( issuer = models.ForeignKey(
'Organizer', 'Organizer',
related_name='gift_card_acceptor_acceptance', related_name='gift_card_collector_acceptance',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
acceptor = models.ForeignKey( collector = models.ForeignKey(
'Organizer', 'Organizer',
related_name='gift_card_issuer_acceptance', related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
active = models.BooleanField(default=True)
reusable_media = models.BooleanField(default=False)
class Meta:
unique_together = (('issuer', 'acceptor'),)
class GiftCard(LoggedModel): class GiftCard(LoggedModel):
@@ -119,7 +114,7 @@ class GiftCard(LoggedModel):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00') return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer): def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, acceptor=organizer, active=True).exists() return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.secret: if not self.secret:

View File

@@ -251,20 +251,14 @@ class Invoice(models.Model):
raise ValueError('Every invoice needs to be connected to an order') raise ValueError('Every invoice needs to be connected to an order')
if not self.event: if not self.event:
self.event = self.order.event self.event = self.order.event
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'event'}.union(kwargs['update_fields'])
if not self.organizer: if not self.organizer:
self.organizer = self.order.event.organizer self.organizer = self.order.event.organizer
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'organizer'}.union(kwargs['update_fields'])
if not self.prefix: if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-') self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if self.is_cancellation: if self.is_cancellation:
self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix
if '%' in self.prefix: if '%' in self.prefix:
self.prefix = self.date.strftime(self.prefix) self.prefix = self.date.strftime(self.prefix)
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'prefix'}.union(kwargs['update_fields'])
if not self.invoice_no: if not self.invoice_no:
if self.order.testmode: if self.order.testmode:
@@ -282,13 +276,8 @@ class Invoice(models.Model):
# Suppress duplicate key errors and try again # Suppress duplicate key errors and try again
if i == 9: if i == 9:
raise raise
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'invoice_no'}.union(kwargs['update_fields'])
if self.full_invoice_no != self.prefix + self.invoice_no:
self.full_invoice_no = self.prefix + self.invoice_no self.full_invoice_no = self.prefix + self.invoice_no
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'full_invoice_no'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):

View File

@@ -40,10 +40,9 @@ from collections import Counter, OrderedDict
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
from decimal import Decimal, DecimalException from decimal import Decimal, DecimalException
from typing import Optional, Tuple from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser import dateutil.parser
from dateutil.tz import datetime_exists import pytz
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import ( from django.core.validators import (
@@ -928,22 +927,22 @@ class Item(LoggedModel):
) )
if self.validity_dynamic_duration_days: if self.validity_dynamic_duration_days:
replace_date += timedelta(days=self.validity_dynamic_duration_days) replace_date += timedelta(days=self.validity_dynamic_duration_days)
valid_until = valid_until.replace( valid_until = tz.localize(valid_until.replace(
year=replace_date.year, year=replace_date.year,
month=replace_date.month, month=replace_date.month,
day=replace_date.day, day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0, hour=23, minute=59, second=59, microsecond=0,
tzinfo=tz, tzinfo=None,
) ))
elif self.validity_dynamic_duration_days: elif self.validity_dynamic_duration_days:
replace_date = valid_until.date() + timedelta(days=self.validity_dynamic_duration_days - 1) replace_date = valid_until.date() + timedelta(days=self.validity_dynamic_duration_days - 1)
valid_until = valid_until.replace( valid_until = tz.localize(valid_until.replace(
year=replace_date.year, year=replace_date.year,
month=replace_date.month, month=replace_date.month,
day=replace_date.day, day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0, hour=23, minute=59, second=59, microsecond=0,
tzinfo=tz tzinfo=None
) ))
if self.validity_dynamic_duration_hours: if self.validity_dynamic_duration_hours:
valid_until += timedelta(hours=self.validity_dynamic_duration_hours) valid_until += timedelta(hours=self.validity_dynamic_duration_hours)
@@ -951,9 +950,6 @@ class Item(LoggedModel):
if self.validity_dynamic_duration_minutes: if self.validity_dynamic_duration_minutes:
valid_until += timedelta(minutes=self.validity_dynamic_duration_minutes) valid_until += timedelta(minutes=self.validity_dynamic_duration_minutes)
if not datetime_exists(valid_until):
valid_until += timedelta(hours=1)
return requested_start, valid_until return requested_start, valid_until
else: else:
@@ -1593,8 +1589,6 @@ class Question(LoggedModel):
if not Question.objects.filter(event=self.event, identifier=code).exists(): if not Question.objects.filter(event=self.event, identifier=code).exists():
self.identifier = code self.identifier = code
break break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.event: if self.event:
self.event.cache.clear() self.event.cache.clear()
@@ -1684,7 +1678,7 @@ class Question(LoggedModel):
try: try:
dt = dateutil.parser.parse(answer) dt = dateutil.parser.parse(answer)
if is_naive(dt): if is_naive(dt):
dt = make_aware(dt, ZoneInfo(self.event.settings.timezone)) dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
except: except:
raise ValidationError(_('Invalid datetime input.')) raise ValidationError(_('Invalid datetime input.'))
else: else:
@@ -1742,8 +1736,6 @@ class QuestionOption(models.Model):
if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists(): if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists():
self.identifier = code self.identifier = code
break break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
@staticmethod @staticmethod

View File

@@ -42,10 +42,10 @@ from collections import Counter
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
from zoneinfo import ZoneInfo
import dateutil import dateutil
import pycountry import pycountry
import pytz
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models, transaction from django.db import models, transaction
@@ -461,20 +461,14 @@ class Order(LockModel, LoggedModel):
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code) return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
def save(self, **kwargs): def save(self, **kwargs):
if 'update_fields' in kwargs: if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields']) kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
if not self.code: if not self.code:
self.assign_code() self.assign_code()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
if not self.datetime: if not self.datetime:
self.datetime = now() self.datetime = now()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'datetime'}.union(kwargs['update_fields'])
if not self.expires: if not self.expires:
self.set_expires() self.set_expires()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'expires'}.union(kwargs['update_fields'])
is_new = not self.pk is_new = not self.pk
update_fields = kwargs.get('update_fields', []) update_fields = kwargs.get('update_fields', [])
@@ -502,7 +496,7 @@ class Order(LockModel, LoggedModel):
def set_expires(self, now_dt=None, subevents=None): def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now() now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone) tz = pytz.timezone(self.event.settings.timezone)
mode = self.event.settings.get('payment_term_mode') mode = self.event.settings.get('payment_term_mode')
if mode == 'days': if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int)) exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
@@ -876,7 +870,7 @@ class Order(LockModel, LoggedModel):
@property @property
def payment_term_last(self): def payment_term_last(self):
tz = ZoneInfo(self.event.settings.timezone) tz = pytz.timezone(self.event.settings.timezone)
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper) term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last: if term_last:
if self.event.has_subevents: if self.event.has_subevents:
@@ -1236,7 +1230,7 @@ class QuestionAnswer(models.Model):
try: try:
d = dateutil.parser.parse(self.answer) d = dateutil.parser.parse(self.answer)
if self.orderposition: if self.orderposition:
tz = ZoneInfo(self.orderposition.order.event.settings.timezone) tz = pytz.timezone(self.orderposition.order.event.settings.timezone)
d = d.astimezone(tz) d = d.astimezone(tz)
return date_format(d, "SHORT_DATETIME_FORMAT") return date_format(d, "SHORT_DATETIME_FORMAT")
except ValueError: except ValueError:
@@ -1448,20 +1442,12 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent)) else self.variation.quotas.filter(subevent=self.subevent))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', set()) update_fields = kwargs.get('update_fields', [])
if 'attendee_name_parts' in update_fields: if 'attendee_name_parts' in update_fields:
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields']) update_fields.append('attendee_name_cached')
self.attendee_name_cached = self.attendee_name
name = self.attendee_name
if name != self.attendee_name_cached:
self.attendee_name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
if self.attendee_name_parts is None: if self.attendee_name_parts is None:
self.attendee_name_parts = {} self.attendee_name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property
@@ -1841,8 +1827,6 @@ class OrderPayment(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.local_id: if not self.local_id:
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1 self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
def create_external_refund(self, amount=None, execution_date=None, info='{}'): def create_external_refund(self, amount=None, execution_date=None, info='{}'):
@@ -2041,8 +2025,6 @@ class OrderRefund(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.local_id: if not self.local_id:
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1 self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
@@ -2461,20 +2443,14 @@ class OrderPosition(AbstractPosition):
assign_ticket_secret( assign_ticket_secret(
event=self.order.event, position=self, force_invalidate=True, save=False event=self.order.event, position=self, force_invalidate=True, save=False
) )
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'secret'}.union(kwargs['update_fields'])
if not self.blocked and self.blocked is not None: if not self.blocked:
self.blocked = None self.blocked = None
if 'update_fields' in kwargs: elif not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked):
kwargs['update_fields'] = {'blocked'}.union(kwargs['update_fields'])
elif self.blocked and (not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked)):
raise TypeError("blocked needs to be a list of strings") raise TypeError("blocked needs to be a list of strings")
if not self.pseudonymization_id: if not self.pseudonymization_id:
self.assign_pseudonymization_id() self.assign_pseudonymization_id()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'pseudonymization_id'}.union(kwargs['update_fields'])
if not self.get_deferred_fields(): if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk: if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
@@ -2960,17 +2936,10 @@ class InvoiceAddress(models.Model):
self.order.touch() self.order.touch()
if self.name_parts: if self.name_parts:
name = self.name
if self.name_cached != name:
self.name_cached = self.name self.name_cached = self.name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else: else:
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = "" self.name_cached = ""
self.name_parts = {} self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs) super().save(**kwargs)
def describe(self): def describe(self):
@@ -3116,6 +3085,10 @@ class BlockedTicketSecret(models.Model):
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)
class Meta: class Meta:
if 'mysql' not in settings.DATABASES['default']['ENGINE']:
# MySQL does not support indexes on TextField(). Django knows this and just ignores db_index, but it will
# not silently ignore the UNIQUE index, causing this table to fail. I'm so glad we're deprecating MySQL
# in a few months, so we'll just live without an unique index until then.
unique_together = (('event', 'secret'),) unique_together = (('event', 'secret'),)

View File

@@ -35,12 +35,12 @@
import string import string
from datetime import date, datetime, time from datetime import date, datetime, time
import pytz_deprecation_shim import pytz
from django.conf import settings from django.conf import settings
from django.core.mail import get_connection from django.core.mail import get_connection
from django.core.validators import MinLengthValidator, RegexValidator from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Exists, OuterRef, Q
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -102,7 +102,6 @@ class Organizer(LoggedModel):
is_new = not self.pk is_new = not self.pk
obj = super().save(*args, **kwargs) obj = super().save(*args, **kwargs)
if is_new: if is_new:
kwargs.pop('update_fields', None) # does not make sense here
self.set_defaults() self.set_defaults()
else: else:
self.get_cache().clear() self.get_cache().clear()
@@ -141,7 +140,7 @@ class Organizer(LoggedModel):
@property @property
def timezone(self): def timezone(self):
return pytz_deprecation_shim.timezone(self.settings.timezone) return pytz.timezone(self.settings.timezone)
@cached_property @cached_property
def all_logentries_link(self): def all_logentries_link(self):
@@ -157,19 +156,17 @@ class Organizer(LoggedModel):
return self.cache.get_or_set( return self.cache.get_or_set(
key='has_gift_cards', key='has_gift_cards',
timeout=15, timeout=15,
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.filter(active=True).exists() default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
) )
@property @property
def accepted_gift_cards(self): def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.filter( return GiftCard.objects.annotate(
Q(issuer=self) | accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
Q(issuer__in=GiftCardAcceptance.objects.filter( ).filter(
acceptor=self, Q(issuer=self) | Q(accepted=True)
active=True,
).values_list('issuer', flat=True))
) )
@property @property

View File

@@ -22,12 +22,9 @@
import json import json
from decimal import Decimal from decimal import Decimal
import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _, pgettext from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField from i18nfield.fields import I18nCharField
@@ -138,25 +135,6 @@ def cc_to_vat_prefix(country_code):
return country_code return country_code
@deconstructible
class CustomRulesValidator:
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/tax-rules-custom.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 set of rules is not valid. Error message: {}').format(e))
class TaxRule(LoggedModel): class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
internal_name = models.CharField( internal_name = models.CharField(

View File

@@ -502,10 +502,7 @@ class Voucher(LoggedModel):
return seat return seat
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.code != self.code.upper():
self.code = self.code.upper() self.code = self.code.upper()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.event.cache.set('vouchers_exist', True) self.event.cache.set('vouchers_exist', True)

View File

@@ -126,19 +126,12 @@ class WaitingListEntry(LoggedModel):
raise ValidationError('Invalid input') raise ValidationError('Invalid input')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', set()) update_fields = kwargs.get('update_fields', [])
if 'name_parts' in update_fields: if 'name_parts' in update_fields:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields']) update_fields.append('name_cached')
name = self.name self.name_cached = self.name
if name != self.name_cached:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
if self.name_parts is None: if self.name_parts is None:
self.name_parts = {} self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property
@@ -218,7 +211,7 @@ class WaitingListEntry(LoggedModel):
'waitinglistentry': self.pk, 'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None, 'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth) }, user=user, auth=auth)
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth) self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
self.voucher = v self.voucher = v
self.save() self.save()

View File

@@ -210,7 +210,7 @@ class SubeventColumn(ImportColumn):
for format in input_formats: for format in input_formats:
try: try:
d = datetime.datetime.strptime(value, format) d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone) d = self.event.timezone.localize(d)
try: try:
se = self.event.subevents.get( se = self.event.subevents.get(
active=True, active=True,
@@ -660,7 +660,7 @@ class ValidFrom(ImportColumn):
for format in input_formats: for format in input_formats:
try: try:
d = datetime.datetime.strptime(value, format) d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone) d = self.event.timezone.localize(d)
return d return d
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
@@ -683,7 +683,7 @@ class ValidUntil(ImportColumn):
for format in input_formats: for format in input_formats:
try: try:
d = datetime.datetime.strptime(value, format) d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone) d = self.event.timezone.localize(d)
return d return d
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass

View File

@@ -39,8 +39,8 @@ import logging
from collections import OrderedDict from collections import OrderedDict
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
from typing import Any, Dict, Union from typing import Any, Dict, Union
from zoneinfo import ZoneInfo
import pytz
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -518,7 +518,7 @@ class BasePaymentProvider:
def _is_still_available(self, now_dt=None, cart_id=None, order=None): def _is_still_available(self, now_dt=None, cart_id=None, order=None):
now_dt = now_dt or now() now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone) tz = pytz.timezone(self.event.settings.timezone)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper) availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date: if availability_date:

View File

@@ -61,6 +61,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pypdf import PdfReader from pypdf import PdfReader
from pytz import timezone
from reportlab.graphics import renderPDF from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing from reportlab.graphics.shapes import Drawing
@@ -236,7 +237,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event begin date and time"), "label": _("Event begin date and time"),
"editor_sample": _("2017-05-31 20:00"), "editor_sample": _("2017-05-31 20:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_from.astimezone(ev.timezone), ev.date_from.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) if ev.date_from else "" ) if ev.date_from else ""
}), }),
@@ -244,7 +245,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event begin date"), "label": _("Event begin date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_from.astimezone(ev.timezone), ev.date_from.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT" "SHORT_DATE_FORMAT"
) if ev.date_from else "" ) if ev.date_from else ""
}), }),
@@ -262,7 +263,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end date and time"), "label": _("Event end date and time"),
"editor_sample": _("2017-05-31 22:00"), "editor_sample": _("2017-05-31 22:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(ev.timezone), ev.date_to.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) if ev.date_to else "" ) if ev.date_to else ""
}), }),
@@ -270,7 +271,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end date"), "label": _("Event end date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(ev.timezone), ev.date_to.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT" "SHORT_DATE_FORMAT"
) if ev.date_to else "" ) if ev.date_to else ""
}), }),
@@ -278,7 +279,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end time"), "label": _("Event end time"),
"editor_sample": _("22:00"), "editor_sample": _("22:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(ev.timezone), ev.date_to.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT" "TIME_FORMAT"
) if ev.date_to else "" ) if ev.date_to else ""
}), }),
@@ -291,7 +292,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event admission date and time"), "label": _("Event admission date and time"),
"editor_sample": _("2017-05-31 19:00"), "editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(ev.timezone), ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) if ev.date_admission else "" ) if ev.date_admission else ""
}), }),
@@ -299,7 +300,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event admission time"), "label": _("Event admission time"),
"editor_sample": _("19:00"), "editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(ev.timezone), ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT" "TIME_FORMAT"
) if ev.date_admission else "" ) if ev.date_admission else ""
}), }),
@@ -384,7 +385,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing date"), "label": _("Printing date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
now().astimezone(ev.timezone), now().astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT" "SHORT_DATE_FORMAT"
) )
}), }),
@@ -392,7 +393,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing date and time"), "label": _("Printing date and time"),
"editor_sample": _("2017-05-31 19:00"), "editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
now().astimezone(ev.timezone), now().astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) )
}), }),
@@ -400,7 +401,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing time"), "label": _("Printing time"),
"editor_sample": _("19:00"), "editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
now().astimezone(ev.timezone), now().astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT" "TIME_FORMAT"
) )
}), }),
@@ -408,7 +409,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start date"), "label": _("Validity start date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(ev.timezone), op.valid_from.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT" "SHORT_DATE_FORMAT"
) if op.valid_from else "" ) if op.valid_from else ""
}), }),
@@ -416,7 +417,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start date and time"), "label": _("Validity start date and time"),
"editor_sample": _("2017-05-31 19:00"), "editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(ev.timezone), op.valid_from.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) if op.valid_from else "" ) if op.valid_from else ""
}), }),
@@ -424,7 +425,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start time"), "label": _("Validity start time"),
"editor_sample": _("19:00"), "editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(ev.timezone), op.valid_from.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT" "TIME_FORMAT"
) if op.valid_from else "" ) if op.valid_from else ""
}), }),
@@ -432,7 +433,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end date"), "label": _("Validity end date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(ev.timezone), op.valid_until.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATE_FORMAT" "SHORT_DATE_FORMAT"
) if op.valid_until else "" ) if op.valid_until else ""
}), }),
@@ -440,7 +441,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end date and time"), "label": _("Validity end date and time"),
"editor_sample": _("2017-05-31 19:00"), "editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(ev.timezone), op.valid_until.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT" "SHORT_DATETIME_FORMAT"
) if op.valid_until else "" ) if op.valid_until else ""
}), }),
@@ -448,7 +449,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end time"), "label": _("Validity end time"),
"editor_sample": _("19:00"), "editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format( "evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(ev.timezone), op.valid_until.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT" "TIME_FORMAT"
) if op.valid_until else "" ) if op.valid_until else ""
}), }),

View File

@@ -22,8 +22,8 @@
import datetime import datetime
from collections import namedtuple from collections import namedtuple
from typing import Union from typing import Union
from zoneinfo import ZoneInfo
import pytz
from dateutil import parser from dateutil import parser
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -67,7 +67,7 @@ class RelativeDateWrapper:
if self.data.minutes_before is not None: if self.data.minutes_before is not None:
raise ValueError('A minute-based relative datetime can not be used as a date') raise ValueError('A minute-based relative datetime can not be used as a date')
tz = ZoneInfo(event.settings.timezone) tz = pytz.timezone(event.settings.timezone)
if isinstance(event, SubEvent): if isinstance(event, SubEvent):
base_date = ( base_date = (
getattr(event, self.data.base_date_name) getattr(event, self.data.base_date_name)
@@ -86,7 +86,7 @@ class RelativeDateWrapper:
if isinstance(self.data, (datetime.datetime, datetime.date)): if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data return self.data
else: else:
tz = ZoneInfo(event.settings.timezone) tz = pytz.timezone(event.settings.timezone)
if isinstance(event, SubEvent): if isinstance(event, SubEvent):
base_date = ( base_date = (
getattr(event, self.data.base_date_name) getattr(event, self.data.base_date_name)
@@ -99,7 +99,8 @@ class RelativeDateWrapper:
if self.data.minutes_before is not None: if self.data.minutes_before is not None:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes_before) return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes_before)
else: else:
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)).astimezone(tz) oldoffset = base_date.astimezone(tz).utcoffset()
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
if self.data.time: if self.data.time:
new_date = new_date.replace( new_date = new_date.replace(
hour=self.data.time.hour, hour=self.data.time.hour,
@@ -107,6 +108,8 @@ class RelativeDateWrapper:
second=self.data.time.second second=self.data.time.second
) )
new_date = new_date.astimezone(tz) new_date = new_date.astimezone(tz)
new_offset = new_date.utcoffset()
new_date += oldoffset - new_offset
return new_date return new_date
def to_string(self) -> str: def to_string(self) -> str:

View File

@@ -32,12 +32,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import os import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from functools import partial, reduce from functools import partial, reduce
import dateutil import dateutil
import dateutil.parser import dateutil.parser
from dateutil.tz import datetime_exists import pytz
from django.core.files import File from django.core.files import File
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import ( from django.db.models import (
@@ -53,8 +53,7 @@ from django.utils.translation import gettext as _
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from pretix.base.models import ( from pretix.base.models import (
Checkin, CheckinList, Device, Event, ItemVariation, Order, OrderPosition, Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
QuestionOption,
) )
from pretix.base.signals import checkin_created, order_placed, periodic_task from pretix.base.signals import checkin_created, order_placed, periodic_task
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
@@ -66,13 +65,12 @@ from pretix.helpers.jsonlogic_query import (
) )
def _build_time(t=None, value=None, ev=None, now_dt=None): def _build_time(t=None, value=None, ev=None):
now_dt = now_dt or now()
if t == "custom": if t == "custom":
return dateutil.parser.parse(value) return dateutil.parser.parse(value)
elif t == "customtime": elif t == "customtime":
parsed = dateutil.parser.parse(value) parsed = dateutil.parser.parse(value)
return now_dt.astimezone(ev.timezone).replace( return now().astimezone(ev.timezone).replace(
hour=parsed.hour, hour=parsed.hour,
minute=parsed.minute, minute=parsed.minute,
second=parsed.second, second=parsed.second,
@@ -86,42 +84,7 @@ def _build_time(t=None, value=None, ev=None, now_dt=None):
return ev.date_admission or ev.date_from return ev.date_admission or ev.date_from
def _logic_annotate_for_graphic_explain(rules, ev, rule_data): def _logic_explain(rules, ev, rule_data):
logic_environment = _get_logic_environment(ev)
event = ev if isinstance(ev, Event) else ev.event
def _evaluate_inners(r):
if not isinstance(r, dict):
return r
operator = list(r.keys())[0]
values = r[operator]
if operator in ("and", "or"):
return {operator: [_evaluate_inners(v) for v in values]}
result = logic_environment.apply(r, rule_data)
return {**r, '__result': result}
def _add_var_values(r):
if not isinstance(r, dict):
return r
operator = [k for k in r.keys() if not k.startswith("__")][0]
values = r[operator]
if operator == "var":
var = values[0] if isinstance(values, list) else values
val = rule_data[var]
if var == "product":
val = str(event.items.get(pk=val))
elif var == "variation":
val = str(ItemVariation.objects.get(item__event=event, pk=val))
elif isinstance(val, datetime):
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
return {"var": var, "__result": val}
else:
return {**r, operator: [_add_var_values(v) for v in values]}
return _add_var_values(_evaluate_inners(rules))
def _logic_explain(rules, ev, rule_data, now_dt=None):
""" """
Explains when the logic denied the check-in. Only works for a denied check-in. Explains when the logic denied the check-in. Only works for a denied check-in.
@@ -151,7 +114,6 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00". get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
""" """
now_dt = now_dt or now()
logic_environment = _get_logic_environment(ev) logic_environment = _get_logic_environment(ev)
_var_values = {'False': False, 'True': True} _var_values = {'False': False, 'True': True}
_var_explanations = {} _var_explanations = {}
@@ -236,9 +198,9 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
else: else:
compare_to -= tolerance compare_to -= tolerance
var_weights[vname] = (200, abs(now_dt - compare_to).total_seconds()) var_weights[vname] = (200, abs(now() - compare_to).total_seconds())
if abs(now_dt - compare_to) < timedelta(hours=12): if abs(now() - compare_to) < timedelta(hours=12):
compare_to_text = date_format(compare_to, 'TIME_FORMAT') compare_to_text = date_format(compare_to, 'TIME_FORMAT')
else: else:
compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT') compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT')
@@ -395,7 +357,7 @@ class LazyRuleVars:
@cached_property @cached_property
def entries_today(self): def entries_today(self):
tz = self._clist.event.timezone tz = self._clist.event.timezone
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0) midnight = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count() return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
@cached_property @cached_property
@@ -416,7 +378,7 @@ class LazyRuleVars:
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least # between platforms (None<1 is true on some, but not all), we rather choose something that is at least
# consistent. # consistent.
return -1 return -1
return (self._dt - last_entry.datetime).total_seconds() // 60 return (now() - last_entry.datetime).total_seconds() // 60
@cached_property @cached_property
def minutes_since_first_entry(self): def minutes_since_first_entry(self):
@@ -428,7 +390,7 @@ class LazyRuleVars:
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least # between platforms (None<1 is true on some, but not all), we rather choose something that is at least
# consistent. # consistent.
return -1 return -1
return (self._dt - last_entry.datetime).total_seconds() // 60 return (now() - last_entry.datetime).total_seconds() // 60
class SQLLogic: class SQLLogic:
@@ -477,7 +439,7 @@ class SQLLogic:
if operator == 'buildTime': if operator == 'buildTime':
if values[0] == "custom": if values[0] == "custom":
return Value(dateutil.parser.parse(values[1]).astimezone(timezone.utc)) return Value(dateutil.parser.parse(values[1]).astimezone(pytz.UTC))
elif values[0] == "customtime": elif values[0] == "customtime":
parsed = dateutil.parser.parse(values[1]) parsed = dateutil.parser.parse(values[1])
return Value(now().astimezone(self.list.event.timezone).replace( return Value(now().astimezone(self.list.event.timezone).replace(
@@ -485,7 +447,7 @@ class SQLLogic:
minute=parsed.minute, minute=parsed.minute,
second=parsed.second, second=parsed.second,
microsecond=parsed.microsecond, microsecond=parsed.microsecond,
).astimezone(timezone.utc)) ).astimezone(pytz.UTC))
elif values[0] == 'date_from': elif values[0] == 'date_from':
return Coalesce( return Coalesce(
F('subevent__date_from'), F('subevent__date_from'),
@@ -513,7 +475,7 @@ class SQLLogic:
return int(values[1]) return int(values[1])
elif operator == 'var': elif operator == 'var':
if values[0] == 'now': if values[0] == 'now':
return Value(now().astimezone(timezone.utc)) return Value(now().astimezone(pytz.UTC))
elif values[0] == 'now_isoweekday': elif values[0] == 'now_isoweekday':
return Value(now().astimezone(self.list.event.timezone).isoweekday()) return Value(now().astimezone(self.list.event.timezone).isoweekday())
elif values[0] == 'product': elif values[0] == 'product':
@@ -731,7 +693,7 @@ def _save_answers(op, answers, given_answers):
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False, def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True, ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY, user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False): raw_barcode=None, raw_source_type=None, from_revoked_secret=False):
""" """
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time. not valid at this time.
@@ -745,7 +707,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
:param questions_supported: When set to False, questions are ignored :param questions_supported: When set to False, questions are ignored
:param nonce: A random nonce to prevent race conditions. :param nonce: A random nonce to prevent race conditions.
:param datetime: The datetime of the checkin, defaults to now. :param datetime: The datetime of the checkin, defaults to now.
:param simulate: If true, the check-in is not saved.
""" """
# !!!!!!!!! # !!!!!!!!!
@@ -773,7 +734,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'blocked' 'blocked'
) )
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt: if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
if force: if force:
force_used = True force_used = True
else: else:
@@ -787,7 +748,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
), ),
) )
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < dt: if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
if force: if force:
force_used = True force_used = True
else: else:
@@ -812,7 +773,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if q not in given_answers and q not in answers: if q not in given_answers and q not in answers:
require_answers.append(q) require_answers.append(q)
if not simulate:
_save_answers(op, answers, given_answers) _save_answers(op, answers, given_answers)
with transaction.atomic(): with transaction.atomic():
@@ -899,9 +859,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
return return
if entry_allowed or force: if entry_allowed or force:
if simulate:
return True
else:
ci = Checkin.objects.create( ci = Checkin.objects.create(
position=op, position=op,
type=type, type=type,
@@ -969,11 +926,14 @@ def process_exit_all(sender, **kwargs):
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
d -= timedelta(hours=1) d -= timedelta(hours=1)
cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}') cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}')
try:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time().replace(fold=1)), cl.event.timezone) cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
if not datetime_exists(cl.exit_all_at): except pytz.exceptions.AmbiguousTimeError:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone,
is_dst=False)
except pytz.exceptions.NonExistentTimeError:
cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True) cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True)
d += timedelta(hours=1) d += timedelta(hours=1)
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time().replace(fold=1)), cl.event.timezone) cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
# AmbiguousTimeError shouldn't be possible since d.time() includes fold=0 # AmbiguousTimeError shouldn't be possible since d.time() includes fold=0
cl.save(update_fields=['exit_all_at']) cl.save(update_fields=['exit_all_at'])

View File

@@ -290,8 +290,6 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
if isinstance(exporter, OrganizerLevelExportMixin): if isinstance(exporter, OrganizerLevelExportMixin):
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission): if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
has_permission = False has_permission = False
if exporter and not exporter.available_for_user(schedule.owner):
has_permission = False
_run_scheduled_export( _run_scheduled_export(
schedule, schedule,

View File

@@ -348,7 +348,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.prefix = None cancellation.prefix = None
cancellation.refers = invoice cancellation.refers = invoice
cancellation.is_cancellation = True cancellation.is_cancellation = True
cancellation.date = timezone.now().astimezone(invoice.event.timezone).date() cancellation.date = timezone.now().date()
cancellation.payment_provider_text = '' cancellation.payment_provider_text = ''
cancellation.payment_provider_stamp = '' cancellation.payment_provider_stamp = ''
cancellation.file = None cancellation.file = None

View File

@@ -45,8 +45,8 @@ from email.mime.image import MIMEImage
from email.utils import formataddr from email.utils import formataddr
from typing import Any, Dict, List, Sequence, Union from typing import Any, Dict, List, Sequence, Union
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
from zoneinfo import ZoneInfo
import pytz
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from celery import chain from celery import chain
@@ -226,11 +226,11 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if event: if event:
timezone = event.timezone timezone = event.timezone
elif user: elif user:
timezone = ZoneInfo(user.timezone) timezone = pytz.timezone(user.timezone)
elif organizer: elif organizer:
timezone = organizer.timezone timezone = organizer.timezone
else: else:
timezone = ZoneInfo(settings.TIME_ZONE) timezone = pytz.timezone(settings.TIME_ZONE)
if settings_holder: if settings_holder:
if settings_holder.settings.mail_bcc: if settings_holder.settings.mail_bcc:

View File

@@ -2019,7 +2019,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_resend_all_links': { 'mail_subject_resend_all_links': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2035,7 +2035,7 @@ The list is as follows:
{orders} {orders}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_free_attendee': { 'mail_subject_order_free_attendee': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2051,7 +2051,7 @@ You can view the details and status of your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_send_order_free_attendee': { 'mail_send_order_free_attendee': {
'type': bool, 'type': bool,
@@ -2072,7 +2072,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_placed_require_approval': { 'mail_subject_order_placed_require_approval': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2090,7 +2090,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_placed': { 'mail_subject_order_placed': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2109,7 +2109,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_attachment_new_order': { 'mail_attachment_new_order': {
'default': None, 'default': None,
@@ -2119,14 +2119,11 @@ Your {event} team""")) # noqa: W291
label=_('Attachment for new orders'), label=_('Attachment for new orders'),
ext_whitelist=(".pdf",), ext_whitelist=(".pdf",),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT, max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT,
help_text=format_lazy( help_text=_('This file will be attached to the first email that we send for every new order. Therefore it will be '
_(
'This file will be attached to the first email that we send for every new order. Therefore it will be '
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent ' 'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use ' 'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
'it to send non-public information as this file might be sent before payment is confirmed or the order ' 'it to send non-public information as this file might be sent before payment is confirmed or the order '
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.' 'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.').format(
),
size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024), size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024),
) )
), ),
@@ -2156,7 +2153,7 @@ You can view the details and status of your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_changed': { 'mail_subject_order_changed': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2172,7 +2169,7 @@ You can view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_paid': { 'mail_subject_order_paid': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2190,7 +2187,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_send_order_paid_attendee': { 'mail_send_order_paid_attendee': {
'type': bool, 'type': bool,
@@ -2210,7 +2207,7 @@ You can view the details and status of your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_days_order_expire_warning': { 'mail_days_order_expire_warning': {
'form_class': forms.IntegerField, 'form_class': forms.IntegerField,
@@ -2243,7 +2240,7 @@ You can view the payment information and the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_pending_warning': { 'mail_subject_order_pending_warning': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2260,7 +2257,7 @@ You can view the payment information and the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_incomplete_payment': { 'mail_subject_order_incomplete_payment': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2280,7 +2277,7 @@ You can view the payment information and the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_waiting_list': { 'mail_subject_waiting_list': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2313,7 +2310,7 @@ as possible to the next person on the waiting list:
{url_remove} {url_remove}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_canceled': { 'mail_subject_order_canceled': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2331,7 +2328,7 @@ You can view the details of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_approved': { 'mail_subject_order_approved': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2351,7 +2348,7 @@ You can select a payment method and perform the payment here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_send_order_approved_attendee': { 'mail_send_order_approved_attendee': {
'type': bool, 'type': bool,
@@ -2371,7 +2368,7 @@ You can view the details and status of your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_approved_free': { 'mail_subject_order_approved_free': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2388,7 +2385,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_send_order_approved_free_attendee': { 'mail_send_order_approved_free_attendee': {
'type': bool, 'type': bool,
@@ -2408,7 +2405,7 @@ You can view the details and status of your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_order_denied': { 'mail_subject_order_denied': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2427,7 +2424,7 @@ You can view the details of your order here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_text_order_custom_mail': { 'mail_text_order_custom_mail': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2437,7 +2434,7 @@ You can change your order details and view the status of your order at
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_days_download_reminder': { 'mail_days_download_reminder': {
'type': int, 'type': int,
@@ -2461,7 +2458,7 @@ If you did not do so already, you can download your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_download_reminder': { 'mail_subject_download_reminder': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2477,7 +2474,7 @@ If you did not do so already, you can download your ticket here:
{url} {url}
Best regards, Best regards,
Your {event} team""")) # noqa: W291 Your {event} team"""))
}, },
'mail_subject_customer_registration': { 'mail_subject_customer_registration': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2499,7 +2496,7 @@ If you did not sign up yourself, please ignore this email.
Best regards, Best regards,
Your {organizer} team""")) # noqa: W291 Your {organizer} team"""))
}, },
'mail_subject_customer_email_change': { 'mail_subject_customer_email_change': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2521,7 +2518,7 @@ If you did not request this, please ignore this email.
Best regards, Best regards,
Your {organizer} team""")) # noqa: W291 Your {organizer} team"""))
}, },
'mail_subject_customer_reset': { 'mail_subject_customer_reset': {
'type': LazyI18nString, 'type': LazyI18nString,
@@ -2543,7 +2540,7 @@ If you did not request a new password, please ignore this email.
Best regards, Best regards,
Your {organizer} team""")) # noqa: W291 Your {organizer} team"""))
}, },
'smtp_use_custom': { 'smtp_use_custom': {
'default': 'False', 'default': 'False',
@@ -3586,10 +3583,8 @@ class SettingsSandbox:
def __delattr__(self, key: str) -> None: def __delattr__(self, key: str) -> None:
del self._event.settings[self._convert_key(key)] del self._event.settings[self._convert_key(key)]
def get(self, key: str, default: Any = None, as_type: type = str, binary_file: bool = False): def get(self, key: str, default: Any = None, as_type: type = str):
return self._event.settings.get( return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type)
self._convert_key(key), default=default, as_type=as_type, binary_file=binary_file
)
def set(self, key: str, value: Any): def set(self, key: str, value: Any):
self._event.settings.set(self._convert_key(key), value) self._event.settings.set(self._convert_key(key), value)

View File

@@ -1,6 +0,0 @@
{# this is the version from django 3.x, prior to https://github.com/django/django/commit/5942ab5eb165ee2e759174e297148a40dd855920 so that django-bootstrap3 can keep doing its magic #}
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>{% endwith %}

View File

@@ -86,11 +86,6 @@
hyphens: auto; hyphens: auto;
} }
p a {
word-wrap: anywhere;
word-break: break-all;
}
.footer { .footer {
padding: 10px; padding: 10px;
text-align: center; text-align: center;
@@ -175,10 +170,6 @@
font-size: 12px; font-size: 12px;
} }
pre, pre code {
white-space: pre-line;
}
{% if rtl %} {% if rtl %}
body { body {
direction: rtl; direction: rtl;

View File

@@ -91,9 +91,12 @@
/* These are technically the same, but use both */ /* These are technically the same, but use both */
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word;
-ms-word-break: break-all; -ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */ /* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto; -ms-hyphens: auto;
@@ -102,15 +105,6 @@
hyphens: auto; hyphens: auto;
} }
p:last-child {
margin-bottom: 0;
}
p a {
word-wrap: anywhere;
word-break: break-all;
}
.footer { .footer {
padding: 10px; padding: 10px;
text-align: center; text-align: center;
@@ -183,7 +177,6 @@
} }
.order { .order {
border-top: 1px solid #ccc;
font-size: 12px; font-size: 12px;
} }
@@ -201,10 +194,6 @@
font-size: 12px; font-size: 12px;
} }
pre, pre code {
white-space: pre-line;
}
{% if rtl %} {% if rtl %}
body { body {
direction: rtl; direction: rtl;

View File

@@ -46,8 +46,6 @@ from django.urls import reverse
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from markdown import Extension
from markdown.inlinepatterns import SubstituteTagInlineProcessor
from tlds import tld_set from tlds import tld_set
register = template.Library() register = template.Library()
@@ -170,21 +168,6 @@ def abslink_callback(attrs, new=False):
return attrs return attrs
class EmailNl2BrExtension(Extension):
"""
In emails (mostly for backwards-compatibility), we do not follow GitHub Flavored Markdown in preserving newlines.
Instead, we follow the CommonMark specification:
"A line ending (not in a code span or HTML tag) that is preceded by two or more spaces and does not occur at the
end of a block is parsed as a hard line break (rendered in HTML as a <br /> tag)"
"""
BR_RE = r' \n'
def extendMarkdown(self, md):
br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
md.inlinePatterns.register(br_tag, 'nl', 5)
def markdown_compile_email(source): def markdown_compile_email(source):
linker = bleach.Linker( linker = bleach.Linker(
url_re=URL_RE, url_re=URL_RE,
@@ -197,7 +180,7 @@ def markdown_compile_email(source):
source, source,
extensions=[ extensions=[
'markdown.extensions.sane_lists', 'markdown.extensions.sane_lists',
EmailNl2BrExtension(), # 'markdown.extensions.nl2br' # disabled for backwards-compatibility
] ]
), ),
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,

View File

@@ -20,10 +20,11 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import calendar import calendar
from datetime import date, datetime, time, timedelta, timezone from datetime import date, datetime, time, timedelta
from itertools import groupby from itertools import groupby
from typing import Optional, Tuple from typing import Optional, Tuple
import pytz
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.formats import date_format from django.utils.formats import date_format
@@ -391,7 +392,7 @@ class SerializerDateFrameField(serializers.CharField):
if data is None: if data is None:
return None return None
try: try:
resolve_timeframe_to_dates_inclusive(now(), data, timezone.utc) resolve_timeframe_to_dates_inclusive(now(), data, pytz.UTC)
except: except:
raise ValidationError("Invalid date frame") raise ValidationError("Invalid date frame")

View File

@@ -21,9 +21,9 @@
# #
import logging import logging
from importlib import import_module from importlib import import_module
from zoneinfo import ZoneInfo
import celery.exceptions import celery.exceptions
import pytz
from celery import states from celery import states
from celery.result import AsyncResult from celery.result import AsyncResult
from django.conf import settings from django.conf import settings
@@ -252,7 +252,7 @@ class AsyncFormView(AsyncMixin, FormView):
task_self = self task_self = self
view_instance._task_self = task_self view_instance._task_self = task_self
with translation.override(locale), timezone.override(ZoneInfo(tz)): with translation.override(locale), timezone.override(pytz.timezone(tz)):
form_class = view_instance.get_form_class() form_class = view_instance.get_form_class()
if form_kwargs.get('instance'): if form_kwargs.get('instance'):
form_kwargs['instance'] = cls.model.objects.get(pk=form_kwargs['instance']) form_kwargs['instance'] = cls.model.objects.get(pk=form_kwargs['instance'])
@@ -302,7 +302,7 @@ class AsyncFormView(AsyncMixin, FormView):
'url_args': self.args, 'url_args': self.args,
'url_kwargs': self.kwargs, 'url_kwargs': self.kwargs,
'locale': get_language(), 'locale': get_language(),
'tz': str(get_current_timezone()), 'tz': get_current_timezone().zone,
} }
if hasattr(self.request, 'organizer'): if hasattr(self.request, 'organizer'):
kwargs['organizer'] = self.request.organizer.pk kwargs['organizer'] = self.request.organizer.pk
@@ -377,7 +377,7 @@ class AsyncPostView(AsyncMixin, View):
task_self = self task_self = self
view_instance._task_self = task_self view_instance._task_self = task_self
with translation.override(locale), timezone.override(ZoneInfo(tz)): with translation.override(locale), timezone.override(pytz.timezone(tz)):
return view_instance.async_post(view_instance.request, *url_args, **url_kwargs) return view_instance.async_post(view_instance.request, *url_args, **url_kwargs)
cls.async_execute = app.task( cls.async_execute = app.task(
@@ -405,7 +405,7 @@ class AsyncPostView(AsyncMixin, View):
'locale': get_language(), 'locale': get_language(),
'url_args': args, 'url_args': args,
'url_kwargs': kwargs, 'url_kwargs': kwargs,
'tz': str(get_current_timezone()), 'tz': get_current_timezone().zone,
} }
if hasattr(self.request, 'organizer'): if hasattr(self.request, 'organizer'):
kwargs['organizer'] = self.request.organizer.pk kwargs['organizer'] = self.request.organizer.pk

View File

@@ -31,8 +31,7 @@ from django_scopes.forms import (
) )
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.models.checkin import CheckinList
from pretix.base.models.checkin import Checkin, CheckinList
from pretix.control.forms import ItemMultipleChoiceField from pretix.control.forms import ItemMultipleChoiceField
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
@@ -178,26 +177,3 @@ class SimpleCheckinListForm(forms.ModelForm):
'subevent': SafeModelChoiceField, 'subevent': SafeModelChoiceField,
'gates': SafeModelMultipleChoiceField, 'gates': SafeModelMultipleChoiceField,
} }
class CheckinListSimulatorForm(forms.Form):
raw_barcode = forms.CharField(
label=_("Barcode"),
)
datetime = forms.SplitDateTimeField(
label=_("Check-in time"),
widget=SplitDateTimePickerWidget(),
)
checkin_type = forms.ChoiceField(
label=_("Check-in type"),
choices=Checkin.CHECKIN_TYPES,
)
ignore_unpaid = forms.BooleanField(
label=_("Allow check-in of unpaid order (if check-in list permits it)"),
required=False,
)
questions_supported = forms.BooleanField(
label=_("Support for check-in questions"),
initial=True,
required=False,
)

View File

@@ -36,7 +36,6 @@
from decimal import Decimal from decimal import Decimal
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
from zoneinfo import ZoneInfo
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@@ -56,7 +55,7 @@ from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import ( from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
) )
from pytz import common_timezones from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
@@ -222,7 +221,7 @@ class EventWizardBasicsForm(I18nModelForm):
}) })
# change timezone # change timezone
zone = ZoneInfo(data.get('timezone')) zone = timezone(data.get('timezone'))
data['date_from'] = self.reset_timezone(zone, data.get('date_from')) data['date_from'] = self.reset_timezone(zone, data.get('date_from'))
data['date_to'] = self.reset_timezone(zone, data.get('date_to')) data['date_to'] = self.reset_timezone(zone, data.get('date_to'))
data['presale_start'] = self.reset_timezone(zone, data.get('presale_start')) data['presale_start'] = self.reset_timezone(zone, data.get('presale_start'))
@@ -231,7 +230,7 @@ class EventWizardBasicsForm(I18nModelForm):
@staticmethod @staticmethod
def reset_timezone(tz, dt): def reset_timezone(tz, dt):
return dt.replace(tzinfo=tz) if dt is not None else None return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
def clean_slug(self): def clean_slug(self):
slug = self.cleaned_data['slug'] slug = self.cleaned_data['slug']
@@ -1262,12 +1261,8 @@ class MailSettingsForm(SettingsForm):
'mail_subject_order_placed_require_approval': ['event', 'order'], 'mail_subject_order_placed_require_approval': ['event', 'order'],
'mail_text_order_approved': ['event', 'order'], 'mail_text_order_approved': ['event', 'order'],
'mail_subject_order_approved': ['event', 'order'], 'mail_subject_order_approved': ['event', 'order'],
'mail_text_order_approved_attendee': ['event', 'order'],
'mail_subject_order_approved_attendee': ['event', 'order'],
'mail_text_order_approved_free': ['event', 'order'], 'mail_text_order_approved_free': ['event', 'order'],
'mail_subject_order_approved_free': ['event', 'order'], 'mail_subject_order_approved_free': ['event', 'order'],
'mail_text_order_approved_free_attendee': ['event', 'order'],
'mail_subject_order_approved_free_attendee': ['event', 'order'],
'mail_text_order_denied': ['event', 'order', 'comment'], 'mail_text_order_denied': ['event', 'order', 'comment'],
'mail_subject_order_denied': ['event', 'order', 'comment'], 'mail_subject_order_denied': ['event', 'order', 'comment'],
'mail_text_order_paid': ['event', 'order', 'payment_info'], 'mail_text_order_paid': ['event', 'order', 'payment_info'],
@@ -1400,7 +1395,7 @@ class CommentForm(I18nModelForm):
fields = ['comment'] fields = ['comment']
widgets = { widgets = {
'comment': forms.Textarea(attrs={ 'comment': forms.Textarea(attrs={
'rows': 6, 'rows': 3,
'class': 'helper-width-100', 'class': 'helper-width-100',
}), }),
} }

View File

@@ -573,11 +573,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
label=_('Sales channel'), label=_('Sales channel'),
required=False, required=False,
) )
has_checkin = forms.NullBooleanField(
required=False,
widget=FilterNullBooleanSelect,
label=_('At least one ticket with check-in'),
)
checkin_attention = forms.NullBooleanField( checkin_attention = forms.NullBooleanField(
required=False, required=False,
widget=FilterNullBooleanSelect, widget=FilterNullBooleanSelect,
@@ -750,12 +745,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
qs = qs.filter( qs = qs.filter(
all_positions__country=fdata.get('attendee_address_country') all_positions__country=fdata.get('attendee_address_country')
).distinct() ).distinct()
if fdata.get('has_checkin') is not None:
qs = qs.annotate(
has_checkin=Exists(
Checkin.all.filter(position__order_id=OuterRef('pk'))
)
).filter(has_checkin=fdata['has_checkin'])
if fdata.get('ticket_secret'): if fdata.get('ticket_secret'):
qs = qs.filter( qs = qs.filter(
all_positions__secret__icontains=fdata.get('ticket_secret') all_positions__secret__icontains=fdata.get('ticket_secret')
@@ -1754,9 +1743,9 @@ class CheckinListAttendeeFilterForm(FilterForm):
label=_('Check-in status'), label=_('Check-in status'),
choices=( choices=(
('', _('All attendees')), ('', _('All attendees')),
('1', _('Checked in')),
('2', pgettext_lazy('checkin state', 'Present')),
('3', pgettext_lazy('checkin state', 'Checked in but left')), ('3', pgettext_lazy('checkin state', 'Checked in but left')),
('2', pgettext_lazy('checkin state', 'Present')),
('1', _('Checked in')),
('0', _('Not checked in')), ('0', _('Not checked in')),
), ),
required=False, required=False,

View File

@@ -747,7 +747,7 @@ class ItemVariationsFormSet(I18nFormSet):
def _should_delete_form(self, form): def _should_delete_form(self, form):
should_delete = super()._should_delete_form(form) should_delete = super()._should_delete_form(form)
if should_delete and form.instance.pk and (form.instance.orderposition_set.exists() or form.instance.cartposition_set.exists()): if should_delete and (form.instance.orderposition_set.exists() or form.instance.cartposition_set.exists()):
form._delete_fail = True form._delete_fail = True
return False return False
return form.cleaned_data.get(DELETION_FIELD_NAME, False) return form.cleaned_data.get(DELETION_FIELD_NAME, False)

View File

@@ -65,8 +65,8 @@ from pretix.base.forms.questions import (
) )
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance, Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
) )
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink from pretix.base.models.organizer import OrganizerFooterLink
@@ -602,7 +602,7 @@ class WebHookForm(forms.ModelForm):
mark_safe('{} <code>{}</code>'.format(a.verbose_name, a.action_type)) mark_safe('{} <code>{}</code>'.format(a.verbose_name, a.action_type))
) for a in get_all_webhook_events().values() ) for a in get_all_webhook_events().values()
] ]
if self.instance and self.instance.pk: if self.instance:
self.fields['events'].initial = list(self.instance.listeners.values_list('action_type', flat=True)) self.fields['events'].initial = list(self.instance.listeners.values_list('action_type', flat=True))
class Meta: class Meta:
@@ -637,11 +637,7 @@ class GiftCardCreateForm(forms.ModelForm):
if GiftCard.objects.filter( if GiftCard.objects.filter(
secret__iexact=s secret__iexact=s
).filter( ).filter(
Q(issuer=self.organizer) | Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self.organizer,
active=True,
).values_list('issuer', flat=True))
).exists(): ).exists():
raise ValidationError( raise ValidationError(
_('A gift card with the same secret already exists in your or an affiliated organizer account.') _('A gift card with the same secret already exists in your or an affiliated organizer account.')
@@ -794,7 +790,6 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
class CustomerUpdateForm(forms.ModelForm): class CustomerUpdateForm(forms.ModelForm):
error_messages = { error_messages = {
'duplicate_identifier': _("An account with this customer ID is already registered."),
'duplicate': _("An account with this email address is already registered."), 'duplicate': _("An account with this email address is already registered."),
} }
@@ -829,7 +824,6 @@ class CustomerUpdateForm(forms.ModelForm):
def clean(self): def clean(self):
email = self.cleaned_data.get('email') email = self.cleaned_data.get('email')
identifier = self.cleaned_data.get('identifier')
if email is not None: if email is not None:
try: try:
@@ -842,17 +836,6 @@ class CustomerUpdateForm(forms.ModelForm):
code='duplicate', code='duplicate',
) )
if identifier is not None:
try:
self.instance.organizer.customers.exclude(pk=self.instance.pk).get(identifier=identifier)
except Customer.DoesNotExist:
pass
else:
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
return self.cleaned_data return self.cleaned_data
@@ -1030,32 +1013,3 @@ class SSOClientForm(I18nModelForm):
else: else:
del self.fields['client_id'] del self.fields['client_id']
del self.fields['regenerate_client_secret'] del self.fields['regenerate_client_secret']
class GiftCardAcceptanceInviteForm(forms.Form):
acceptor = forms.CharField(
label=_("Organizer short name"),
required=True,
)
reusable_media = forms.BooleanField(
label=_("Allow access to reusable media"),
help_text=_("This is required if you want the other organizer to participate in a shared system with e.g. "
"NFC payment chips. You should only use this option for organizers you trust, since (depending "
"on the activated medium types) this will grant the other organizer access to cryptographic key "
"material required to interact with the media type."),
required=False,
)
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
def clean_acceptor(self):
val = self.cleaned_data['acceptor']
try:
acceptor = Organizer.objects.exclude(pk=self.organizer.pk).get(slug=val)
except Organizer.DoesNotExist:
raise ValidationError(_('The selected organizer does not exist or cannot be invited.'))
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
raise ValidationError(_('The selected organizer has already been invited.'))
return acceptor

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value from bootstrap3.text import text_value
from django.forms import CheckboxInput from django.forms import CheckboxInput
from django.forms.utils import flatatt from django.forms.utils import flatatt
@@ -27,8 +28,6 @@ from django.utils.safestring import mark_safe
from django.utils.translation import pgettext from django.utils.translation import pgettext
from i18nfield.forms import I18nFormField from i18nfield.forms import I18nFormField
from pretix.base.forms.renderers import FieldRenderer, InlineFieldRenderer
def render_label(content, label_for=None, label_class=None, label_title='', optional=False): def render_label(content, label_for=None, label_class=None, label_title='', optional=False):
""" """

View File

@@ -271,7 +271,7 @@ class VoucherBulkForm(VoucherForm):
required=False, required=False,
initial=_('Hello,\n\n' initial=_('Hello,\n\n'
'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n' 'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n'
'You can redeem them here in our ticket shop:\n\n{url}\n\nBest regards, \n' 'You can redeem them here in our ticket shop:\n\n{url}\n\nBest regards,\n\n'
'Your {event} team') 'Your {event} team')
) )
send_recipients = forms.CharField( send_recipients = forms.CharField(

View File

@@ -39,6 +39,7 @@ from decimal import Decimal
import bleach import bleach
import dateutil.parser import dateutil.parser
import pytz
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.formats import date_format from django.utils.formats import date_format
@@ -208,7 +209,7 @@ def _display_checkin(event, logentry):
if 'datetime' in data: if 'datetime' in data:
dt = dateutil.parser.parse(data.get('datetime')) dt = dateutil.parser.parse(data.get('datetime'))
show_dt = abs((logentry.datetime - dt).total_seconds()) > 5 or 'forced' in data show_dt = abs((logentry.datetime - dt).total_seconds()) > 5 or 'forced' in data
tz = event.timezone tz = pytz.timezone(event.settings.timezone)
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT") dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data: if 'list' in data:
@@ -340,9 +341,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'), '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.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
'pretix.webhook.created': _('The webhook has been created.'), 'pretix.webhook.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'), 'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'), 'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
@@ -531,10 +529,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.permissions.invited': _('A user has been invited to the event team.'), 'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'), 'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'), 'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy 'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'),
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'), 'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy 'pretix.event.orders.waitinglist.transferred': _('An entry has been transferred to another waiting list.'),
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'), 'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'), 'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
'pretix.team.created': _('The team has been created.'), 'pretix.team.created': _('The team has been created.'),
@@ -630,7 +627,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.control.views.checkin': if logentry.action_type == 'pretix.control.views.checkin':
# deprecated # deprecated
dt = dateutil.parser.parse(data.get('datetime')) dt = dateutil.parser.parse(data.get('datetime'))
tz = sender.timezone tz = pytz.timezone(sender.settings.timezone)
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT") dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data: if 'list' in data:
try: try:

View File

@@ -519,32 +519,13 @@ def get_organizer_navigation(request):
}) })
if 'can_manage_gift_cards' in request.orgapermset: if 'can_manage_gift_cards' in request.orgapermset:
children = []
children.append({
'label': _('Gift cards'),
'url': reverse('control:organizer.giftcards', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
'children': children,
})
if 'can_change_organizer_settings' in request.orgapermset:
children.append(
{
'label': _('Acceptance'),
'url': reverse('control:organizer.giftcards.acceptance', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.giftcards.acceptance' in url.url_name,
}
)
nav.append({ nav.append({
'label': _('Gift cards'), 'label': _('Gift cards'),
'url': reverse('control:organizer.giftcards', kwargs={ 'url': reverse('control:organizer.giftcards', kwargs={
'organizer': request.organizer.slug 'organizer': request.organizer.slug
}), }),
'active': 'organizer.giftcard' in url.url_name,
'icon': 'credit-card', 'icon': 'credit-card',
'children': children,
}) })
if request.organizer.settings.customer_accounts: if request.organizer.settings.customer_accounts:

View File

@@ -423,6 +423,17 @@
</div> </div>
{% endif %} {% 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 %} {% if debug_warning %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "pretix is running in debug mode. For security reasons, please never run debug mode on a production instance." %} {% trans "pretix is running in debug mode. For security reasons, please never run debug mode on a production instance." %}

View File

@@ -16,17 +16,12 @@
{% trans "Edit list configuration" %} {% trans "Edit list configuration" %}
</a> </a>
{% endif %} {% endif %}
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}" <a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
class="btn btn-default">
<span class="fa fa-flask"></span>
{% trans "Check-in simulator" %}
</a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}{% if "status" in request.GET %}&checkinlistpdf-status={{ request.GET.status|urlencode }}{% endif %}"
class="btn btn-default" target="_blank"> class="btn btn-default" target="_blank">
<span class="fa fa-download"></span> <span class="fa fa-download"></span>
{% trans "PDF" %} {% trans "PDF" %}
</a> </a>
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}{% if "status" in request.GET %}&checkinlist-status={{ request.GET.status|urlencode }}{% endif %}" <a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}"
class="btn btn-default" target="_blank"> class="btn btn-default" target="_blank">
<span class="fa fa-download"></span> <span class="fa fa-download"></span>
{% trans "CSV" %} {% trans "CSV" %}

View File

@@ -12,15 +12,7 @@
{% endblock %} {% endblock %}
{% block inside %} {% block inside %}
{% if checkinlist %} {% if checkinlist %}
<h1> <h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
target="_blank"
class="btn btn-default">
<span class="fa fa-flask"></span>
{% trans "Check-in simulator" %}
</a>
</h1>
{% else %} {% else %}
<h1>{% trans "Check-in list" %}</h1> <h1>{% trans "Check-in list" %}</h1>
{% endif %} {% endif %}

View File

@@ -133,9 +133,6 @@
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip"> class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span> <span class="fa fa-copy"></span>
</a> </a>
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a> <a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %} {% endif %}

View File

@@ -1,140 +0,0 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load getitem %}
{% load static %}
{% load compress %}
{% block title %}{% trans "Check-in simulator" %}{% endblock %}
{% block inside %}
<h1>
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% if 'can_change_event_settings' in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
class="btn btn-default">
<span class="fa fa-wrench"></span>
{% trans "Edit list configuration" %}
</a>
{% endif %}
</h1>
<h2>{% trans "Check-in simulator" %}</h2>
<p>
{% blocktrans trimmed %}
This tool allows you to validate your check-in configuration. You can enter a barcode plus some
optional parameters and we will show you the response of the check-in list. No actual check-in will
be performed and no modification to the system state is made.
{% endblocktrans %}
</p>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.raw_barcode layout="control" %}
{% bootstrap_field form.datetime layout="control" %}
{% bootstrap_field form.checkin_type layout="control" %}
{% bootstrap_field form.ignore_unpaid layout="control" %}
{% bootstrap_field form.questions_supported layout="control" %}
<div class="row">
<div class="col-md-9 col-md-offset-3">
<button type="submit" class="btn btn-primary">
{% trans "Simulate" %}
</button>
</div>
</div>
</form>
{% if result %}
<hr>
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-title">{% trans "Result" %}</div>
</div>
<div class="panel-body checkin-sim-result checkin-sim-result-status-{{ result.status }} checkin-sim-result-reason-{{ result.reason }}">
{% if result.status == "ok" %}
<span class="fa fa-check-circle"></span>
{% elif result.status == "incomplete" %}
<span class="fa fa-question-circle"></span>
{% elif result.status == "error" %}
{% if result.reason == "already_redeemed" %}
<span class="fa fa-warning"></span>
{% else %}
<span class="fa fa-exclamation-circle"></span>
{% endif %}
{% endif %}
</div>
<div class="panel-body">
{% if result.status == "ok" %}
<h3 class="nomargin-top">{% trans "Valid check-in" %}</h3>
{% elif result.status == "incomplete" %}
<h3 class="nomargin-top">{% trans "Additional information required" %}</h3>
<p>
{% trans "The following questions must be answered before check-in can be completed:" %}
</p>
<ul>
{% for q in result.questions %}
<li>
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
{{ q.question }}
</a>
</li>
{% endfor %}
</ul>
{% elif result.status == "error" %}
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
{% if result.reason_explanation %}
<p>{{ result.reason_explanation }}</p>
{% endif %}
{% endif %}
{% if result.position %}
{% if result.position.require_attention %}
<p>
<span class="fa fa-info-circle fa-fw"></span> {% trans "Special attention required" %}
</p>
{% endif %}
<p>
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=result.position.order %}">
{{ result.position.order }}</a>-{{ result.position.positionid }}
</p>
{% if result.position.attendee_name %}
<p>
<span class="fa fa-user fa-fw"></span>
{{ result.position.attendee_name }}
</p>
{% endif %}
{% endif %}
{% if result.rule_graph %}
<div id="rules-editor" class="form-inline">
<div role="tabpanel" class="tab-pane" id="rules-viz">
<checkin-rules-visualization></checkin-rules-visualization>
</div>
<textarea id="id_rules" class="sr-only">{{ result.rule_graph|attr_escapejson_dumps }}</textarea>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if DEBUG %}
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
{% else %}
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-dispatch.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-ease.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-interpolate.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-selection.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-timer.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/viz-node.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-visualization.vue' %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules.js" %}"></script>
{% endcompress %}
{% endblock %}

View File

@@ -6,12 +6,6 @@
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}"> <a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }} {{ gc.secret }}
</a> </a>
{% if gc.issuer != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
</dd> </dd>
<dt>{% trans "Issuer" %}</dt> <dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd> <dd>{{ gc.issuer }}</dd>

View File

@@ -1,19 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load money %}
{% block inner %}
<h1>
{% trans "Invite organizer" %}
</h1>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,150 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load money %}
{% block inner %}
<h1>
{% trans "Gift cards acceptance" %}
</h1>
<p>
{% blocktrans trimmed %}
This feature allows you to configure acceptance of gift cards across multiple organizer accounts.
{% endblocktrans %}
</p>
<form method="post">
{% csrf_token %}
<h2>
{% trans "Other organizers you accept gift cards from" %}
</h2>
{% if issuer_acceptance|length == 0 and not filter_form.filtered %}
<p>
{% blocktrans trimmed %}
You are not accepting gift cards from other organizers yet. If you want to do so, the other
organizer can add you to their list and afterwards, you can confirm this here.
{% endblocktrans %}
</p>
{% else %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Organizer" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Reusable media" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for gca in issuer_acceptance %}
<tr>
<td>
{{ gca.issuer.name }}<br><code>{{ gca.issuer.slug }}</code>
</td>
<td>
{% if gca.active %}
{% trans "active" %}
{% else %}
{% trans "invited" %}
{% endif %}
</td>
<td>
{% if gca.reusable_media %}
{% trans "active" %}
{% else %}
{% trans "disabled" %}
{% endif %}
</td>
<td class="text-right">
{% if gca.active %}
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
{% trans "Remove" %}
</button>
{% else %}
<button class="btn btn-success" name="accept_issuer" value="{{ gca.issuer.slug }}">
{% trans "Accept" %}
</button>
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
{% trans "Decline" %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</form>
<form method="post">
{% csrf_token %}
<h2>
{% trans "Other organizers accepting gift cards from you" %}
</h2>
<p>
{% blocktrans trimmed %}
You can invite other organizers to accept your gift cards. After you have done so, they need to go
to the same page in their account and accept your invitation. Note that other organizers will be able
to add money to gift cards as well that you will need to collect form them. It is your responsibility
to handle the exchange of money to offset the transactions between the two organizers.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can optionally control whether they can access your reusable media. This is required if you want
them to participate in a shared system with e.g. NFC payment chips.
{% endblocktrans %}
{% blocktrans trimmed %}
You should only use this option for organizers you trust, since (depending on the activated medium types)
this will grant the other organizer access to cryptographic key material required to interact with
the media type.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.giftcards.acceptance.invite" organizer=request.organizer.slug %}" class="btn btn-default">
{% trans "Invite new organizer" %}
</a>
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Organizer" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Reusable media" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for gca in acceptor_acceptance %}
<tr>
<td>
{{ gca.acceptor.name }}<br><code>{{ gca.acceptor.slug }}</code>
</td>
<td>
{% if gca.active %}
{% trans "active" %}
{% else %}
{% trans "invited" %}
{% endif %}
</td>
<td>
{% if gca.reusable_media %}
{% trans "active" %}
{% else %}
{% trans "disabled" %}
{% endif %}
</td>
<td class="text-right">
<button class="btn btn-danger" name="delete_acceptor" value="{{ gca.acceptor.slug }}">
{% trans "Remove" %}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -99,4 +99,44 @@
</div> </div>
{% include "pretixcontrol/pagination.html" %} {% include "pretixcontrol/pagination.html" %}
{% endif %} {% endif %}
{% if not is_paginated or page_obj.number == 1 %}
<form action="" method="post" class="form-inline">
{% csrf_token %}
<fieldset>
<legend>{% trans "Accepted gift cards of other organizers" %}</legend>
<p>
{% blocktrans trimmed %}
If you have access to multiple organizer accounts, you can configure that ticket shops in
this account will also accept gift codes issued through a different organizer account, and
vice versa.
{% endblocktrans %}
</p>
<ul>
{% for gca in request.organizer.gift_card_issuer_acceptance.all %}
<li>
<strong>{{ gca.issuer }}</strong>
<button type="submit" name="del" value="{{ gca.issuer.slug }}" class="btn btn-xs btn-danger">
<span class="fa fa-trash"></span>
</button>
</li>
{% empty %}
<li>
<em>{% trans "You are currently not accepting gift cards from other organizers." %}</em>
</li>
{% endfor %}
{% if other_organizers %}
<li>
<select name="add" class="form-control input-sm">
<option></option>
{% for o in other_organizers %}
<option value="{{ o.slug }}">{{ o }}</option>
{% endfor %}
</select>
<button class="btn btn-primary btn-sm" type="submit"><span class="fa fa-plus"></span></button>
</li>
{% endif %}
</ul>
</fieldset>
</form>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -130,10 +130,7 @@
</td> </td>
<td> <td>
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}"> <strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
{{ s.name }}</a></strong><br> {{ s.name }}</a></strong>
<small class="text-muted">
#{{ s.pk }}
</small>
</td> </td>
<td> <td>
{{ s.get_date_from_display }}<br> {{ s.get_date_from_display }}<br>

View File

@@ -21,7 +21,7 @@
{% if redeemed_in_carts %} {% if redeemed_in_carts %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% blocktrans trimmed with number=redeemed_in_carts %} {% blocktrans trimmed with number=redeemed_in_carts %}
This voucher is currently used in {{ number }} cart sessions and might not be free to use until the cart sessions This voucher is currently used in {{ number }} cart sessions and there might not be free to use until the cart sessions
expire. expire.
{% endblocktrans %} {% endblocktrans %}
<p class="text-right"> <p class="text-right">

View File

@@ -33,7 +33,8 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
from django.urls import include, re_path from django.conf.urls import re_path
from django.urls import include
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from pretix.control.views import ( from pretix.control.views import (
@@ -176,10 +177,6 @@ urlpatterns = [
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'), re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(),
name='organizer.giftcard.edit'), name='organizer.giftcard.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance$', organizer.GiftCardAcceptanceListView.as_view(),
name='organizer.giftcards.acceptance'),
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance/invite$', organizer.GiftCardAcceptanceInviteView.as_view(),
name='organizer.giftcards.acceptance.invite'),
re_path(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'), re_path(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
name='organizer.webhook.add'), name='organizer.webhook.add'),
@@ -433,7 +430,6 @@ urlpatterns = [
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'), re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'), re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'), re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P<list>\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'), re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(), re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
name='event.orders.checkinlists.edit'), name='event.orders.checkinlists.edit'),

View File

@@ -31,8 +31,6 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import secrets
from datetime import timezone
import dateutil.parser import dateutil.parser
from django.contrib import messages from django.contrib import messages
@@ -45,21 +43,15 @@ from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware, now from django.utils.timezone import is_aware, make_aware, now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, ListView from django.views.generic import ListView
from i18nfield.strings import LazyI18nString from pytz import UTC
from pretix.api.views.checkin import _redeem_process
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, Order, OrderPosition from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList from pretix.base.models.checkin import CheckinList
from pretix.base.services.checkin import (
LazyRuleVars, _logic_annotate_for_graphic_explain,
)
from pretix.base.signals import checkin_created from pretix.base.signals import checkin_created
from pretix.base.views.tasks import AsyncPostView from pretix.base.views.tasks import AsyncPostView
from pretix.control.forms.checkin import ( from pretix.control.forms.checkin import CheckinListForm
CheckinListForm, CheckinListSimulatorForm,
)
from pretix.control.forms.filter import ( from pretix.control.forms.filter import (
CheckinFilterForm, CheckinListAttendeeFilterForm, CheckinFilterForm, CheckinListAttendeeFilterForm,
) )
@@ -171,20 +163,20 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
if e.last_entry: if e.last_entry:
if isinstance(e.last_entry, str): if isinstance(e.last_entry, str):
# Apparently only happens on SQLite # Apparently only happens on SQLite
e.last_entry_aware = make_aware(dateutil.parser.parse(e.last_entry), timezone.utc) e.last_entry_aware = make_aware(dateutil.parser.parse(e.last_entry), UTC)
elif not is_aware(e.last_entry): elif not is_aware(e.last_entry):
# Apparently only happens on MySQL # Apparently only happens on MySQL
e.last_entry_aware = make_aware(e.last_entry, timezone.utc) e.last_entry_aware = make_aware(e.last_entry, UTC)
else: else:
# This would be correct, so guess on which database it works… Yes, it's PostgreSQL. # This would be correct, so guess on which database it works… Yes, it's PostgreSQL.
e.last_entry_aware = e.last_entry e.last_entry_aware = e.last_entry
if e.last_exit: if e.last_exit:
if isinstance(e.last_exit, str): if isinstance(e.last_exit, str):
# Apparently only happens on SQLite # Apparently only happens on SQLite
e.last_exit_aware = make_aware(dateutil.parser.parse(e.last_exit), timezone.utc) e.last_exit_aware = make_aware(dateutil.parser.parse(e.last_exit), UTC)
elif not is_aware(e.last_exit): elif not is_aware(e.last_exit):
# Apparently only happens on MySQL # Apparently only happens on MySQL
e.last_exit_aware = make_aware(e.last_exit, timezone.utc) e.last_exit_aware = make_aware(e.last_exit, UTC)
else: else:
# This would be correct, so guess on which database it works… Yes, it's PostgreSQL. # This would be correct, so guess on which database it works… Yes, it's PostgreSQL.
e.last_exit_aware = e.last_exit e.last_exit_aware = e.last_exit
@@ -477,63 +469,3 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
ctx = super().get_context_data() ctx = super().get_context_data()
ctx['filter_form'] = self.filter_form ctx['filter_form'] = self.filter_form
return ctx return ctx
class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/checkin/simulator.html'
permission = 'can_view_orders'
form_class = CheckinListSimulatorForm
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
self.result = None
r = super().dispatch(request, *args, **kwargs)
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
return r
def get_initial(self):
return {
'datetime': now()
}
def get_context_data(self, **kwargs):
return super().get_context_data(
**kwargs,
checkinlist=self.list,
result=self.result,
reason_labels=dict(Checkin.REASONS),
)
def form_valid(self, form):
self.result = _redeem_process(
checkinlists=[self.list],
raw_barcode=form.cleaned_data["raw_barcode"],
answers_data={},
datetime=form.cleaned_data["datetime"],
force=False,
checkin_type=form.cleaned_data["checkin_type"],
ignore_unpaid=form.cleaned_data["ignore_unpaid"],
untrusted_input=True,
user=self.request.user,
auth=None,
expand=[],
nonce=secrets.token_hex(12),
pdf_data=False,
questions_supported=form.cleaned_data["questions_supported"],
canceled_supported=False,
request=self.request, # this is not clean, but we need it in the serializers for URL generation
legacy_url_support=False,
simulate=True,
).data
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data)
self.result["rule_graph"] = rule_graph
if self.result.get("questions"):
for q in self.result["questions"]:
q["question"] = LazyI18nString(q["question"])
return self.get(self.request, self.args, self.kwargs)

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