forked from CGM_Public/pretix_original
Compare commits
4 Commits
api-taxrul
...
hide-empty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1e082ee3 | ||
|
|
9a57371f9e | ||
|
|
8bc7045bba | ||
|
|
3c29223e5c |
4
.github/workflows/style.yml
vendored
4
.github/workflows/style.yml
vendored
@@ -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
|
||||||
|
|||||||
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ Listing available exporters
|
|||||||
"input_parameters": [
|
"input_parameters": [
|
||||||
{
|
{
|
||||||
"name": "events",
|
"name": "events",
|
||||||
"required": false
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "_format",
|
"name": "_format",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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``
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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!
|
||||||
|
|||||||
@@ -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']:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 {}
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'),)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ""
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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',
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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." %}
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user