Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
8fa715ac4b API: Allow to add debug_data to failed check-ins 2023-12-01 11:09:18 +01:00
481 changed files with 216402 additions and 265562 deletions

View File

@@ -10,9 +10,7 @@ updates:
schedule:
interval: "daily"
versioning-strategy: increase
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:
interval: "monthly"
open-pull-requests-limit: 5

View File

@@ -26,12 +26,12 @@ jobs:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

View File

@@ -25,12 +25,12 @@ jobs:
name: Spellcheck
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

View File

@@ -23,12 +23,12 @@ jobs:
runs-on: ubuntu-22.04
name: Check gettext syntax
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -48,12 +48,12 @@ jobs:
runs-on: ubuntu-22.04
name: Spellcheck
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

View File

@@ -23,12 +23,12 @@ jobs:
name: isort
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -43,12 +43,12 @@ jobs:
name: flake8
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -63,9 +63,9 @@ jobs:
name: licenseheaders
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: 3.11
- name: Install Dependencies

View File

@@ -32,7 +32,7 @@ jobs:
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '15'
@@ -41,10 +41,10 @@ jobs:
postgresql password: 'postgres'
if: matrix.database == 'postgres'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v4
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

View File

@@ -1,30 +1,29 @@
before_script:
tests:
image:
name: pretix/ci-image
stage: test
before_script:
- pip install -U pip uv
- uv pip install --system -U wheel setuptools
script:
- uv pip install --system -e ".[dev]"
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- cd src
- python manage.py check
- make all compress
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
- py.test --reruns 3 -n 3 tests
tags:
- python3
except:
- pypi
pypi:
stage: release
image:
name: pretix/ci-image
before_script:
- cat $PYPIRC > ~/.pypirc
- pip install -U pip uv
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
script:
- uv pip install --system -e ".[dev]"
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python setup.py sdist
- uv pip install --system dist/pretix-*.tar.gz
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- cd src
@@ -34,12 +33,13 @@ pypi:
- python -m build
- twine check dist/*
- twine upload dist/*
tags:
- python3
only:
- pypi
artifacts:
paths:
- src/dist/
stages:
- test
- build

View File

@@ -42,6 +42,7 @@ Example::
currency=EUR
datadir=/data
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
cookie_domain=.pretix.de
``instance_name``
The name of this installation. Default: ``pretix.de``
@@ -52,18 +53,10 @@ Example::
``currency``
The default currency as a three-letter code. Defaults to ``EUR``.
``cachedir``
The local path to a directory where temporary files will be stored.
Defaults to the ``cache`` directory below the ``datadir``.
``datadir``
The local path to a data directory that will be used for storing user uploads and similar
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
``logdir``
The local path to a directory where log files will be stored.
Defaults to the ``logs`` directory below the ``datadir``.
``plugins_default``
A comma-separated list of plugins that are enabled by default for all new events.
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
@@ -78,6 +71,9 @@ Example::
``auth_backends``
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.
``registration``
Enables or disables the registration of new admin users. Defaults to ``off``.
@@ -97,9 +93,8 @@ Example::
Defaults to ``off``.
``obligatory_2fa``
Enables or disables obligatory usage of two-factor authentication for users of the pretix backend.
Can be ``True`` to make two-factor authentication obligatory for all users or ``staff`` to make it only
obligatory to users with admin permissions. Defaults to ``False``.
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
Defaults to ``False``
``trust_x_forwarded_for``
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
@@ -158,7 +153,6 @@ Example::
host=localhost
port=3306
advisory_lock_index=1
disable_server_side_cursors=0
sslmode=require
sslrootcert=/etc/pretix/postgresql-ca.crt
sslcert=/etc/pretix/postgresql-client-crt.crt
@@ -179,11 +173,6 @@ Example::
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
you should set separate values for this setting (integers up to 256).
``disable_server_side_cursors``
On PostgreSQL pretix might use server side cursors for certain operations. This is generally fine but will break in
specific circumstances, for example when connecting to PostgreSQL through a PGBouncer configured with a transaction
pool mode. Off by default (i.e. by default server side cursors will be used).
``sslmode``, ``sslrootcert``
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
@@ -360,7 +349,7 @@ to speed up various operations::
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
or ``unix://[:password]@/path/to/socket.sock?db=0``
``sessions``
``session``
When this is set to ``True``, redis will be used as the session storage.
``sentinels``
@@ -536,4 +525,4 @@ pretix can optionally make use of a GeoIP database for some features. It needs a
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data

View File

@@ -1,18 +0,0 @@
.. highlight:: none
.. _`community`:
Community install guides
========================
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
Kubernetes
----------
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
Docker
------
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal

View File

@@ -14,4 +14,3 @@ for your needs.
manual_smallscale
dev_version
enterprise
community

View File

@@ -120,7 +120,6 @@ Now we will install pretix itself. The following steps are to be executed as the
actually install pretix, we will create a virtual environment to isolate the python packages from your global
python installation::
# sudo -u pretix -s
$ python3 -m venv /var/pretix/venv
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
@@ -280,7 +279,6 @@ Updates
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
(venv)$ python -m pretix migrate

View File

@@ -103,12 +103,6 @@ pretix_celery_tasks_queued_count
pretix_celery_tasks_queued_age_seconds
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
pretix_logins_successful
Counter. The number of successful backend logins.
pretix_logins_failed
Counter. The number of failed backend logins, labeled with ``reason``.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/
.. _cProfile: https://docs.python.org/3/library/profile.html

View File

@@ -249,10 +249,7 @@ You can get three response codes:
Content-Type: application/json
{
"event": {
"name": "Demo Conference",
"slug": "democon"
},
"event": "democon",
"subevent": 23,
"checkinlist": 5
}

View File

@@ -94,9 +94,7 @@ If you want the user to return to your application after the payment is complete
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
either enter ``https://example.org/order/`` or ``https://example.org/``.
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error

View File

@@ -19,7 +19,6 @@ external_identifier string External ID of
the API, but is read-only for customers created through a
SSO integration.
email string Customer email address
phone string Customer phone number
name string Name of this customer (or ``null``)
name_parts object of strings Decomposition of name (i.e. given name, family name)
is_active boolean Whether this account is active
@@ -40,10 +39,6 @@ password string Can only be set
Passwords can now be set through the API during customer creation.
.. versionchanged:: 2024.3
The attribute ``phone`` has been added.
Endpoints
---------
@@ -76,7 +71,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -124,7 +118,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -162,7 +155,6 @@ Endpoints
{
"email": "test@example.org",
"phone": "+493012345678",
"password": "verysecret",
"send_email": true
}
@@ -179,7 +171,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
...
}
@@ -224,7 +215,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
}
@@ -259,7 +249,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": null,
"phone": null,
}

View File

@@ -36,8 +36,6 @@ geo_lon float Longitude of th
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data object Values set for organizer-specific meta data parameters.
The allowed keys need to be set up as meta properties
in the organizer configuration.
plugins list A list of package names of the enabled plugins for this
event.
seating_plan integer If reserved seating is in use, the ID of a seating
@@ -345,8 +343,8 @@ Endpoints
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the ``plugins``, ``has_subevents``, ``meta_data`` and/or ``is_public`` fields are present in the post body this will
determine their value. Otherwise their value will be copied from the existing event.
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
value. Otherwise their value will be copied from the existing event.
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
when creating a new event for this instead.

View File

@@ -45,16 +45,8 @@ sales_channels list of strings Sales channels
available.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_from setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this variation can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_until setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -113,9 +105,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {
"en": "Test2"
@@ -141,9 +131,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {},
"position": 1,
@@ -204,9 +192,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -246,9 +232,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -279,9 +263,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -343,9 +325,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 1,

View File

@@ -50,16 +50,8 @@ sales_channels list of strings Sales channel
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this item can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
@@ -164,16 +156,8 @@ variations list of objects A list with o
available.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ available_until datetime The last date time at which this variation can be bought
(or ``null``).
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -295,9 +279,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -342,9 +324,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -364,9 +344,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -439,9 +417,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -487,9 +463,7 @@ Endpoints
"description": null,
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"meta_data": {},
"position": 0
@@ -508,9 +482,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -564,9 +536,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -610,9 +580,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -632,9 +600,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -676,9 +642,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -723,9 +687,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -745,9 +707,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -820,9 +780,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -867,9 +825,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -889,9 +845,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},

View File

@@ -137,17 +137,13 @@ last_modified datetime Last modificati
The ``event`` attribute has been added. The organizer-level endpoint has been added.
.. versionchanged:: 2023.9
The ``customer`` query parameter has been added.
.. versionchanged:: 2023.10
The ``checkin_text`` attribute has been added.
.. versionchanged:: 2024.1
.. versionchanged:: 2023.9
The ``expires`` attribute can now be passed during order creation.
The ``customer`` query parameter has been added.
.. _order-position-resource:
@@ -179,11 +175,6 @@ country string Attendee countr
state string Attendee state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
voucher integer Internal ID of the voucher used for this position (or ``null``)
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
to how much of the ``budget`` of the voucher is consumed.
**Important:** Do not rely on this amount to be a useful
value if the position's price, product or voucher
are changed *after* the order was created. Can be ``null``.
tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position
tax_rule integer The ID of the used tax rule (or ``null``)
@@ -372,7 +363,6 @@ List of all orders
"country": "DE",
"state": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_value": "0.00",
"tax_rule": null,
@@ -595,7 +585,6 @@ Fetching individual orders
"country": "DE",
"state": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
@@ -740,8 +729,6 @@ Updating order fields
* ``valid_if_pending``
* ``expires``
**Example request**:
.. sourcecode:: http
@@ -1548,7 +1535,6 @@ List of all order positions
},
"attendee_email": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
@@ -1662,7 +1648,6 @@ Fetching individual positions
},
"attendee_email": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",

View File

@@ -22,8 +22,6 @@ id integer Internal ID of
name string Team name
all_events boolean Whether this team has access to all events
limit_events list List of event slugs this team has access to
require_2fa boolean Whether members of this team are required to use
two-factor authentication
can_create_events boolean
can_change_teams boolean
can_change_organizer_settings boolean
@@ -124,7 +122,6 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -162,7 +159,6 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -190,7 +186,6 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -208,7 +203,6 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}
@@ -252,7 +246,6 @@ Team endpoints
"name": "Admin team",
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
...
}

View File

@@ -13,8 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
register_ticket_secret_generators, gift_card_transaction_display
Order events
""""""""""""

View File

@@ -3,12 +3,11 @@
.. _`importcol`:
Extending the import process
============================
Extending the order import process
==================================
It's possible through the backend to import objects into pretix, for example orders from a legacy ticketing system. If
your plugin defines additional data structures around those objects, it might be useful to make it possible to import
them as well.
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
Import process
--------------
@@ -41,7 +40,7 @@ Column registration
The import API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available import columns. Your plugin
should listen for this signal and return the subclass of ``pretix.base.modelimport.ImportColumn``
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
that we'll provide in this plugin:
.. sourcecode:: python
@@ -57,16 +56,10 @@ that we'll provide in this plugin:
EmailColumn(sender),
]
Similar signals exist for other objects:
.. automodule:: pretix.base.signals
:members: voucher_import_columns
The column class API
--------------------
.. class:: pretix.base.modelimport.ImportColumn
.. class:: pretix.base.orderimport.ImportColumn
The central object of each import extension is the subclass of ``ImportColumn``.

View File

@@ -84,8 +84,6 @@ convenient to you:
.. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page

View File

@@ -1,11 +1,10 @@
.. highlight:: python
:linenothreshold: 5
Writing a template placeholder plugin
=====================================
Writing an e-mail placeholder plugin
====================================
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
configurable texts.
An email placeholder is a dynamic value that pretix users can use in their email templates.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
@@ -13,31 +12,31 @@ Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
.. code-block:: python
from django.dispatch import receiver
from pretix.base.signals import register_text_placeholders
from pretix.base.signals import register_mail_placeholders
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
def register_placeholder_renderers(sender, **kwargs):
from .placeholders import MyPlaceholderClass
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
Not all placeholders make sense in every email, and placeholders usually depend some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in a context where all those dependencies are
what values they depend on and they will only be available in an email if all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
@@ -52,7 +51,7 @@ There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. autoattribute:: identifier
@@ -78,15 +77,7 @@ functions:
.. code-block:: python
placeholder = SimpleFunctionalTextPlaceholder(
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)
Signals
-------
.. automodule:: pretix.base.signals
:members: register_text_placeholders
.. automodule:: pretix.base.signals
:members: register_mail_placeholders

View File

@@ -19,4 +19,3 @@ Contents:
permissions
logging
locking
timemachine

View File

@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
* ``LazyI18nString``
* References to Django ``File`` objects that are already stored in a storage backend [#f1]_
* References to Django ``File`` objects that are already stored in a storage backend
* References to model instances
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
@@ -55,9 +55,6 @@ You can simply use it like this:
"preserve his reservation."),
)
.. _settings-defaults-in-plugins:
Defaults in plugins
-------------------
@@ -73,9 +70,3 @@ Make sure that you include this code in a module that is imported at app loading
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
.. rubric:: Footnotes
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
user copies the settings of an existing event to a new one.

View File

@@ -1,32 +0,0 @@
Time machine mode
=================
In test mode, pretix provides a "time machine" feature which allows event organizers
to test their shop as if it were a different date and time. To enable this feature, they can
click on the "time machine"-link in the test mode warning box on the event page.
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
taken into account. If you add code that uses the current date and time for checking whether some
product can be bought, you should use :py:meth:`time_machine_now`.
.. autofunction:: pretix.base.timemachine.time_machine_now
Background tasks
----------------
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
Therefore, if you call a background task in the order process, where time_machine_now should be
respected, you need to pass it through manually as shown in the example below:
.. code-block:: python
@app.task()
def my_task(self, override_now_dt: datetime=None) -> None:
with time_machine_now_assigned(override_now_dt):
# ...do something that uses time_machine_now()
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned

View File

@@ -90,10 +90,6 @@ as its first argument and can be used like this::
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
{% load eventurl %}
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
Implementation details
----------------------

View File

@@ -211,15 +211,5 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
whenever you change a source file.
Working with frontend assets
----------------------------
To update the frontend styles of shops with a custom styling, run the following commands inside
your virtual environment.::
python -m pretix collectstatic --noinput
python -m pretix updatestyles
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
.. _pretixdroid: https://github.com/pretix/pretixdroid

View File

@@ -31,7 +31,7 @@ pretix/
Additional code implementing our customized :ref:`URL handling <urlconf>`.
static/
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
@@ -41,6 +41,6 @@ pretix/
tests/
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
testing.

View File

@@ -32,7 +32,6 @@ transactions list of objects Transactions in
├ checksum string Checksum computed from payer, reference, amount and
date
├ payer string Payment source
├ external_id string Unique ID of the payment from an external source
├ reference string Payment reference
├ amount string Payment amount
├ iban string Payment IBAN
@@ -86,7 +85,6 @@ Endpoints
"date": "26.06.2017",
"payer": "John Doe",
"order": null,
"external_id": null,
"iban": "",
"bic": "",
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
@@ -141,7 +139,6 @@ Endpoints
"iban": "",
"bic": "",
"order": null,
"external_id": null,
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
"state": "nomatch",

View File

@@ -34,19 +34,13 @@ internal_id string Can be used for
contact_name string Contact person (or ``null``)
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
contact_email string Contact person email address (or ``null``)
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
booth string Booth number (or ``null``). Maximum 100 characters.
locale string Locale for communication with the exhibitor.
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
lead_scanning_access_code string Access code for the exhibitor to use the lead scanning app but not access data (read-only).
allow_lead_scanning boolean Enables lead scanning app
allow_lead_access boolean Enables access to data gathered by the lead scanning app
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
per scanning device, instead of only per exhibitor.
comment string Internal comment, not shown to exhibitor
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
The tags need to be created through the web interface currently.
===================================== ========================== =======================================================
You can also access the scanned leads through the API which contains the following public fields:
@@ -68,7 +62,6 @@ data list of objects Attendee data s
except in a few cases where it contains an additional list of objects
with ``value`` and ``label`` keys (e.g. splitting of names).
device_name string User-defined name for the device used for scanning (or ``null``).
device_uuid string UUID of device used for scanning (or ``null``).
===================================== ========================== =======================================================
Endpoints
@@ -112,17 +105,13 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
"comment": "",
"exhibitor_tags": []
"comment": ""
}
]
}
@@ -167,17 +156,13 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
"comment": "",
"exhibitor_tags": []
"comment": ""
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -372,16 +357,12 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
"comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
"comment": ""
}
**Example response**:
@@ -405,19 +386,13 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
"comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
"comment": ""
}
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
@@ -469,19 +444,13 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
"comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
"comment": ""
}
:param organizer: The ``slug`` field of the organizer to modify
@@ -592,7 +561,6 @@ name string Exhibitor name
booth string Booth number (or ``null``)
event object Object describing the event
├ name multi-lingual string Event name
├ end_date datetime End date of the event. After this time, the app could show a warning that the event is over.
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
@@ -628,7 +596,6 @@ scan_types list of objects Only used for a
"booth": "A2",
"event": {
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
"end_date": "2017-12-28T10:00:00+00:00",
"slug": "bigevents",
"imprint_url": null,
"privacy_url": null,
@@ -667,7 +634,6 @@ On the request, you should set the following properties:
* ``tags`` with the list of selected tags
* ``rating`` with the rating assigned by the exhibitor
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
* ``device_uuid`` with a auto-generated UUID of the device used for scanning, or ``null``
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
responds with the previously saved information and will not delete that information. If you
@@ -702,8 +668,7 @@ The request for this looks like this:
"scan_type": "lead",
"tags": ["foo"],
"rating": 4,
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"device_name": "DEV1"
}
**Example response:**
@@ -736,9 +701,7 @@ The request for this looks like this:
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"notes": "Great customer, wants our newsletter"
}
:statuscode 200: No error, leads was not scanned for the first time
@@ -793,9 +756,7 @@ You can also fetch existing leads (if you are authorized to do so):
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"notes": "Great customer, wants our newsletter"
}
]
}

View File

@@ -1,4 +1,4 @@
sphinx==7.3.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -1,5 +1,5 @@
-e ../
sphinx==7.3.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -1,113 +0,0 @@
Android version support policy
==============================
Building software for Android always presents a struggle between keeping compatibility with older hardware to save cost
and utilizing feature of new Android versions to improve functionality, security and stability. To help you plan ahead,
we are publishing our intended schedule. This is to be understood as a minimum commitment, we will only drop support for
older versions if there is a technical reason to do so, not because the scheduled time has been reached.
.. warning:: This is a non-binding document. We will try our very best to not to deprecate support for Android versions
earlier than listed here, but for technical or economical reasons, it might become necessary to do so under
specific circumstances. Specifically, we might be forced to partially drop support for Android versions
earlier where we integrate third-party components into our software. Typical examples would be specific
payment terminal or printer types where we use a third-party component provided by the hardware vendor.
If we no longer support an Android version, it means that we will no longer publish new versions of the app supporting
that Android version. This means you are not getting new features or bug fixes, and at some point your app might stop
working with the pretix server.
pretixSCAN
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPOS
---------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 | Support planned until at least 12/2029.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 13 | Support planned until at least 12/2028.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 12 | Support planned until at least 12/2027.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 11 | Support planned until at least 12/2026.
| No support for Swissbit microSD TSE.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 | Support planned until at least 12/2025.
| Support for Stripe Terminal on some devices to be dropped 05/2024.
Android 7 | Support planned until at least 12/2024.
| Support for Stripe Terminal to be dropped 05/2024.
| No support for Cryptovision TSE.
Android 6 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
Android 5 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
| No support for SumUp.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPRINT
-----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for Evolis printers on some devices.
Android 4.4 | Support planned until at least 06/2024.
| No support for USB printers.
| No support for Evolis printers.
Android 4 Support dropped.
=========================== ==========================================================
pretixLEAD
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 12/2024.
Android 6 Support planned until at least 12/2024.
Android 5 Support planned until at least 12/2024.
Android 4 Support dropped.
=========================== ==========================================================

View File

@@ -194,23 +194,17 @@ A complete record could look like this::
v=spf1 a mx include:_spf.pretix.eu ~all
Make sure to read up on the `SPF specification`_.
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
record for the subdomain ``pretix._domainkey`` with the following contents::
If you want to authenticate your emails with `DKIM`_, set up a ``CNAME`` record for the subdomain ``pretix._domainkey``
pointing to ``dkim.pretix.eu``::
pretix._domainkey.mydomain.com. CNAME dkim.pretix.eu.
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
For senders with larger volumes, Google Mail also requires you to have a `DMARC`_ policy (that may however be ``p=none``).
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
in this case, as pretix only retries email delivery for a certain time period.
.. _DKIM: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
.. _DMARC: https://en.wikipedia.org/wiki/DMARC

View File

@@ -19,3 +19,4 @@ Then, head to the **Bundled products** tab of the "conference ticket" and add th
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
You can find more use cases in these specialized guides:

View File

@@ -17,8 +17,8 @@ and then click "Generate widget code".
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css" crossorigin>
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async crossorigin></script>
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
The second snippet should be embedded at the position where the widget should show up::
@@ -138,7 +138,7 @@ the button-style of that checkbox with the one in the pretix shop, you can use t
.. note::
Due to compatibility with existing widget installations, the default value for ``single-item-select``
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
``single-item-select="checkbox"`` if you need it.
@@ -196,7 +196,7 @@ settings. For example, if you set up a meta data property called "Promoted" that
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
If you have enabled public filters in your meta data attribute configuration, a filter-form shows up. To disable, use::
If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
(see above). ``country`` expects a two-character country code.
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
country code.
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
@@ -429,34 +429,4 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
});
</script>
Offering wallet payments (Apple Pay, Google Pay) within the widget
------------------------------------------------------------------
Some payment providers (such as Stripe) also offer Apple or Google Pay. But in order to use them, the domain of the
payment needs to be approved first. As of right now, pretix will take care of the domain verification process for you
automatically, when using Stripe. However, pretix can only validate the domain that is being used for your default,
"stand-alone" shop (such as https://pretix.eu/demo/democon/ ).
When embedding the widget on your website, the domain of the embedding page will also need to be validated in order to
be able to use it for wallet payments.
The details might vary from payment provider to payment provider, but generally speaking, it will either involve just
telling your payment provider the domain name and (for Apple Pay) placing an
``apple-developer-merchantid-domain-association``-file into the ``.well-known``-directory of your domain.
Further reading:
* `Stripe Payment Method Domain registration`_
Working with Cross-Origin-Embedder-Policy
-----------------------------------------
The pretix widget is unfortunately not compatible with ``Cross-Origin-Embedder-Policy: require-corp``. If you include
the ``crossorigin`` attributes on the ``<script>`` and ``<link>`` tag as shown above, the widget can show a calendar
or product list, but will not be able to open the checkout process in an iframe. If you also set
``Cross-Origin-Opener-Policy: same-origin``, the widget can auto-detect that it is running in an isolated enviroment
and will instead open the checkout process in a new tab.
.. _Let's Encrypt: https://letsencrypt.org/
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration

View File

@@ -16,5 +16,4 @@ wanting to use pretix to sell tickets.
events/giftcards
faq
markdown
android-version-support
glossary

View File

@@ -11,9 +11,6 @@ In many places of your shop, like frontpage texts, product descriptions and emai
since it is way easier to learn than languages like HTML but allows all basic formatting options required
for text in those places.
.. note:: Some fields that are used in one-line context only allow formatting that refers to individual words
(such as bold or italic font or a link) but do not allow block-level formatting like lists or headlines.
Formatting rules
----------------
@@ -148,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
security reasons you can only use the following HTML elements::
a, abbr, acronym, b, br, code, div, em, h1, h2,
h3, h4, h5, h6, hr, i, li, ol, p, pre, s, span, strong,
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
table, tbody, td, thead, tr, ul
Additionally, only the following attributes are allowed on them::

View File

@@ -30,42 +30,42 @@ dependencies = [
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.4.*",
"chardet==5.2.*",
"celery==5.3.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.14.*",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django[argon2]==4.2.*",
"django-bootstrap3==24.2",
"django-compressor==4.5",
"django-countries==7.6.*",
"django-filter==24.2",
"Django==4.2.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==1.2.*",
"django-hijack==3.5.*",
"django-formtools==2.4.1",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.5.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-oauth-toolkit==2.2.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==2.0.*",
"django-statici18n==2.5.*",
"djangorestframework==3.15.*",
"dnspython==2.6.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.3.*",
"libsass==0.23.*",
"libsass==0.22.*",
"lxml",
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.2.*",
@@ -73,61 +73,63 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.8.*",
"PyJWT==2.7.*",
"phonenumberslite==8.13.*",
"Pillow==10.3.*",
"Pillow==9.5.*",
"pretix-plugin-build",
"protobuf==5.27.*",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==4.2.*",
"pycparser==2.21",
"pycryptodome==3.18.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"reportlab==4.2.*",
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==2.5.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
"stripe==7.9.*",
"stripe==5.4.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==0.18.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.1.*",
"webauthn==0.4.*",
"zeep==4.2.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.9.*",
"aiohttp==3.8.*",
"coverage",
"coveralls",
"fakeredis==2.23.*",
"flake8==7.1.*",
"fakeredis==2.18.*",
"flake8==6.0.*",
"freezegun",
"isort==5.13.*",
"pep8-naming==0.14.*",
"isort==5.12.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
"pyflakes==3.0.*",
"pytest-asyncio",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.6.*",
"pytest==8.2.*",
"pytest-xdist==3.3.*",
"pytest==7.3.*",
"responses",
]

View File

@@ -1,4 +0,0 @@
{
"ignore_dirs": ["node_modules", "data", "pretix/static", "pretix/locale", "pretix/static.dist"]
}

View File

@@ -6,7 +6,7 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.6.0.dev0"
__version__ = "2023.11.0.dev0"

View File

@@ -79,7 +79,6 @@ ALL_LANGUAGES = [
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('ca', _('Catalan')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')),
@@ -99,7 +98,6 @@ ALL_LANGUAGES = [
('pt-br', _('Portuguese (Brazil)')),
('ro', _('Romanian')),
('ru', _('Russian')),
('sk', _('Slovak')),
('es', _('Spanish')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
@@ -113,7 +111,6 @@ LANGUAGES_RTL = {
LANGUAGES_INCUBATING = {
'fi', 'pt-br', 'gl',
}
LANGUAGES = ALL_LANGUAGES
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
]
@@ -237,12 +234,7 @@ COMPRESS_FILTERS = {
)
}
CURRENCIES = [
c for c in currencies
if c.alpha_3 not in {
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
]
CURRENCIES = list(currencies)
CURRENCY_PLACES = {
# default is 2
'BIF': 0,
@@ -275,10 +267,9 @@ CACHE_LARGE_VALUES_ALIAS = 'default'
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", ".jpg", ".gif", ".jpeg")
PILLOW_FORMATS_QUESTIONS_FAVICON = ('PNG', 'GIF', 'JPEG', 'ICO')
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", ".jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
@@ -287,5 +278,3 @@ FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
".bmp", ".tif", ".tiff"
)
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
PRETIX_MAX_ORDER_SIZE = 500

View File

@@ -38,7 +38,6 @@ MAIL_FROM_ORGANIZERS = 'invalid@invalid'
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
FILE_UPLOAD_MAX_SIZE_FAVICON = 10
DEFAULT_CURRENCY = 'EUR'
SECRET_KEY = "build-time-secret-key"
HAS_REDIS = False

View File

@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django.contrib.auth.models import AnonymousUser
from django_scopes import scopes_disabled
from rest_framework import exceptions
@@ -31,8 +29,6 @@ from pretix.api.auth.devicesecurity import (
)
from pretix.base.models import Device
logger = logging.getLogger(__name__)
class DeviceTokenAuthentication(TokenAuthentication):
model = Device
@@ -50,7 +46,6 @@ class DeviceTokenAuthentication(TokenAuthentication):
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if device.revoked:
logging.warning(f'Connection attempt of revoked device {device.pk}.')
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device

View File

@@ -185,7 +185,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:order-mark-canceled'),
@@ -224,7 +223,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)

View File

@@ -39,8 +39,7 @@ from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.security import (
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
SessionReauthRequired, assert_session_valid,
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
@@ -67,10 +66,6 @@ class EventPermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
else request.user)
@@ -149,10 +144,6 @@ class ProfilePermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
if isinstance(request.auth, OAuthAccessToken):
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
@@ -175,9 +166,5 @@ class AnyAuthenticatedClientPermission(BasePermission):
return False
except SessionReauthRequired:
return False
except Session2FASetupRequired:
return False
except SessionPasswordChangeRequired:
return False
return True

View File

@@ -1,49 +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 rest_framework import exceptions
from rest_framework.authentication import (
SessionAuthentication as BaseSessionAuthentication,
)
from pretix.multidomain.middlewares import CsrfViewMiddleware
class CustomCSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class SessionAuthentication(BaseSessionAuthentication):
# Override from DRF to user our custom CSRF middleware
def enforce_csrf(self, request):
def dummy_get_response(request): # pragma: no cover
return None
check = CustomCSRFCheck(dummy_get_response)
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)

View File

@@ -54,7 +54,7 @@ class IdempotencyMiddleware:
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.10 on 2024-02-12 11:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixapi", "0011_bigint"),
]
operations = [
migrations.AddField(
model_name="oauthapplication",
name="post_logout_redirect_uris",
field=models.TextField(default=""),
),
]

View File

@@ -42,11 +42,6 @@ class OAuthApplication(AbstractApplication):
verbose_name=_("Redirection URIs"),
help_text=_("Allowed URIs list, space separated")
)
post_logout_redirect_uris = models.TextField(
blank=True, validators=[URIValidator],
help_text=_("Allowed Post Logout URIs list, space separated"),
default="",
)
client_id = models.CharField(
verbose_name=_("Client ID"),
max_length=100, unique=True, default=generate_client_id, db_index=True

View File

@@ -424,7 +424,7 @@ class CloneEventSerializer(EventSerializer):
new_event = super().create({**validated_data, 'plugins': None})
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
new_event.copy_data_from(event)
if plugins is not None:
new_event.set_active_plugins(plugins)
@@ -472,8 +472,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state',
'comment')
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -684,12 +683,10 @@ class EventSettingsSerializer(SettingsSerializer):
'locales',
'locale',
'region',
'allow_modifications',
'allow_modifications_after_checkin',
'last_order_modification_date',
'allow_modifications_after_checkin',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
@@ -736,7 +733,6 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
'payment_giftcard__enabled',
'mail_days_order_expire_warning',
'ticket_download',
'ticket_download_date',

View File

@@ -61,8 +61,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -86,8 +85,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -237,8 +235,7 @@ class ItemSerializer(I18nAwareModelSerializer):
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
'personalized', 'position', 'picture',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'personalized', 'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',

View File

@@ -486,11 +486,11 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
'valid_from', 'valid_until', 'blocked')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked'
)
def __init__(self, *args, **kwargs):
@@ -564,8 +564,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
attendee_name = AttendeeNameField(source='*')
attendee_name_parts = AttendeeNamePartsField(source='*')
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
class Meta:
model = OrderPosition
@@ -573,8 +571,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
'blocked')
'order__status', 'valid_from', 'valid_until', 'blocked')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1038,14 +1035,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
self.fields['expires'].required = False
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires')
'require_approval', 'valid_if_pending')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1054,11 +1050,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_expires(self, expires):
if expires < now():
raise ValidationError('Expiration date must be in the future.')
return expires
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
@@ -1080,10 +1071,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'An order cannot be empty.'
)
if len(data) > settings.PRETIX_MAX_ORDER_SIZE:
raise ValidationError(
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
)
errs = [{} for p in data]
if any([p.get('positionid') for p in data]):
if not all([p.get('positionid') for p in data]):
@@ -1318,7 +1305,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
valid_from, valid_until = pos_data['item'].compute_validity(
requested_start=(
requested_valid_from
max(requested_valid_from, now())
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
else now()
),
@@ -1369,8 +1356,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
if not validated_data.get('expires'):
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
if validated_data.get('require_approval') is not None:
@@ -1442,7 +1428,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
else:
pos._calculate_tax(invoice_address=ia)
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = listed_price
@@ -1470,7 +1455,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax(invoice_address=ia)
pos._calculate_tax()
if simulate:
pos = WrappedModel(pos)
@@ -1589,10 +1574,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
@@ -1604,8 +1586,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
if validated_data.get('require_approval'):
raise ValidationError('You cannot create a paid order that requires approval.')
order.payments.create(
amount=order.total,
provider=payment_provider,

View File

@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
class Meta:
model = Customer
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
'locale', 'last_modified', 'notes')
def update(self, instance, validated_data):
if instance and instance.provider_id:
@@ -239,7 +239,7 @@ class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = (
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'

View File

@@ -89,7 +89,6 @@ class SettingsSerializer(serializers.Serializer):
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fname.name)
instance.delete(attr)
self.changed_data.append(attr)
else:
# file is unchanged
continue

View File

@@ -35,7 +35,6 @@ from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
@@ -153,6 +152,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'], url_name='failed_checkins')
@transaction.atomic()
def failed_checkins(self, *args, **kwargs):
additional_log_data = {}
if 'debug_data' in self.request.data:
# Intentionally undocumented, might be removed again
additional_log_data['debug_data'] = self.request.data.pop('debug_data')
serializer = FailedCheckinSerializer(
data=self.request.data,
context={'event': self.request.event}
@@ -195,14 +199,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
'reason_explanation': c.error_explanation,
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk
'list': c.list.pk,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
else:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk,
'barcode': c.raw_barcode
'barcode': c.raw_barcode,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
return Response(serializer.data, status=201)
@@ -286,8 +292,6 @@ with scopes_disabled():
return queryset.filter(last_checked_in__isnull=not value)
def check_rules_qs(self, queryset, name, value):
if not value:
return queryset
if not self.checkinlist.rules:
return queryset
return queryset.filter(
@@ -587,32 +591,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
if media.linked_orderposition.order.event_id not in list_by_event:
# Medium exists but connected ticket is for the wrong event
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
error_explanation=gettext('Medium connected to other event'),
**common_checkin_args,
)
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': gettext('Medium connected to other event'),
'require_attention': False,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())

View File

@@ -190,10 +190,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
@transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
current_live_value = serializer.instance.live
updated_live_value = serializer.validated_data.get('live', None)
current_plugins_value = serializer.instance.get_plugins()
@@ -201,11 +198,6 @@ class EventViewSet(viewsets.ModelViewSet):
super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
if updated_live_value is not None and updated_live_value != current_live_value:
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
serializer.instance.log_action(
@@ -262,7 +254,7 @@ class EventViewSet(viewsets.ModelViewSet):
new_event = serializer.save(organizer=self.request.organizer)
if copy_from:
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
new_event.copy_data_from(copy_from)
if plugins is not None:
new_event.set_active_plugins(plugins)
@@ -299,7 +291,7 @@ class EventViewSet(viewsets.ModelViewSet):
try:
with transaction.atomic():
instance.organizer.log_action(
'pretix.event.deleted', user=self.request.user, auth=self.request.auth,
'pretix.event.deleted', user=self.request.user,
data={
'event_id': instance.pk,
'name': str(instance.name),
@@ -630,12 +622,11 @@ class EventSettingsView(views.APIView):
s.is_valid(raise_exception=True)
with transaction.atomic():
s.save()
if s.changed_data:
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_css.apply_async(args=(request.event.pk,))
s = EventSettingsSerializer(

View File

@@ -42,7 +42,7 @@ class IdempotencyQueryView(APIView):
idempotency_key = request.GET.get("key")
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
if not idempotency_key:

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import datetime
import logging
import mimetypes
import os
from decimal import Decimal
@@ -28,7 +27,7 @@ from zoneinfo import ZoneInfo
import django_filters
from django.conf import settings
from django.db import IntegrityError, transaction
from django.db import transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
)
@@ -97,9 +96,6 @@ from pretix.base.signals import (
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__)
with scopes_disabled():
class OrderFilter(FilterSet):
@@ -226,8 +222,6 @@ class OrderViewSetMixin:
qs = qs.prefetch_related('refunds', 'refunds__payment')
if 'invoice_address' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('invoice_address')
if 'customer' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('customer')
qs = qs.prefetch_related(self._positions_prefetch(self.request))
return qs
@@ -576,10 +570,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
@transaction.atomic()
def create_invoice(self, request, **kwargs):
order = self.get_object()
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
has_inv = order.invoices.exists() and not (
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
@@ -906,11 +898,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
def perform_create(self, serializer):
try:
serializer.save()
except IntegrityError:
logger.exception("Integrity error while saving order")
raise ValidationError("Integrity error, possibly duplicate submission of same order.")
serializer.save()
def perform_destroy(self, instance):
if not instance.testmode:
@@ -1908,7 +1896,6 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=204)
@action(detail=True, methods=['POST'])
@transaction.atomic()
def reissue(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
@@ -1916,10 +1903,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv)
if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(order)
inv = generate_invoice(inv.order)
else:
inv = c
inv.order.log_action(

View File

@@ -176,7 +176,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
@@ -185,7 +185,6 @@ class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['type'] = logentry.parsed_data.get('type')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
return d
@@ -297,11 +296,11 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.denied',
_('Order denied'),
),
ParametrizedOrderPositionCheckinWebhookEvent(
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
),
ParametrizedOrderPositionCheckinWebhookEvent(
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin.reverted',
_('Ticket check-in reverted'),
),
@@ -385,7 +384,7 @@ def register_default_webhook_events(sender, **kwargs):
def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer_link').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:

View File

@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .services import auth, checkin, currencies, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings

View File

@@ -19,7 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -30,21 +33,21 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
)
from pretix.base.services.placeholders import ( # noqa
BaseTextPlaceholder as BaseMailTextPlaceholder,
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
)
from pretix.base.settings import get_name_parts_localized # noqa
logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
@@ -189,7 +192,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
tpl = get_template(self.template_name)
body_html = tpl.render(htmlctx)
inliner = css_inline.CSSInliner(keep_style_tags=False)
inliner = css_inline.CSSInliner(remove_style_tags=True)
body_html = inliner.inline(body_html)
return body_html
@@ -214,5 +217,495 @@ def base_renderers(sender, **kwargs):
return [ClassicMailRenderer, UnembellishedMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all()
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'position' in kwargs:
kwargs.setdefault("position_or_address", kwargs['position'])
if 'order' in kwargs:
try:
if not kwargs.get('invoice_address'):
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
finally:
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
try:
ctx[v.identifier] = v.render(kwargs)
except:
ctx[v.identifier] = '(error)'
logger.exception(f'Failed to process email placeholder {v.identifier}.')
return ctx
def _placeholder_payments(order, payments):
d = []
for payment in payments:
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
else:
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
d = [line for line in d if line.strip()]
if d:
return '\n\n'.join(d)
else:
return ''
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalMailTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalMailTextPlaceholder(
'pending_sum', ['event', 'pending_sum'],
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalMailTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalMailTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalMailTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalMailTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
if "concatenation_for_salutation" in name_scheme:
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph

View File

@@ -28,5 +28,4 @@ from .items import * # noqa
from .json import * # noqa
from .mail import * # noqa
from .orderlist import * # noqa
from .reusablemedia import * # noqa
from .waitinglist import * # noqa

View File

@@ -39,12 +39,10 @@ from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import QuestionAnswer
from ...control.forms.widgets import Select2
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -58,7 +56,7 @@ class AnswerFilesExporter(BaseExporter):
@property
def export_form_fields(self):
d = OrderedDict(
return OrderedDict(
[
('questions',
forms.ModelMultipleChoiceField(
@@ -71,32 +69,11 @@ class AnswerFilesExporter(BaseExporter):
)),
]
)
if self.event.has_subevents:
d['subevent'] = forms.ModelChoiceField(
label=pgettext_lazy('subevent', 'Date'),
queryset=self.event.subevents.all(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
d['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
d['subevent'].widget.choices = d['subevent'].choices
return d
def render(self, form_data: dict):
qs = QuestionAnswer.objects.filter(
orderposition__order__event=self.event,
).select_related('orderposition', 'orderposition__order', 'question')
if form_data.get('subevent'):
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:

View File

@@ -116,29 +116,15 @@ class DekodiNREIExporter(BaseExporter):
'PTNo15': p.full_id or '',
})
elif p.provider and p.provider.startswith('stripe'):
pi = p.info_data or {}
try:
if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict):
details = pi["latest_charge"]["payment_method_details"]
card = details.get("card", {})
elif pi.get("charges") and pi["charges"]["data"]:
details = pi["charges"]["data"][0].get("payment_method_details", {})
card = details.get("card", {})
else:
details = pi["source"]
card = pi["source"]["card"]
except:
details = {}
card = {}
src = p.info_data.get("source", p.info_data)
payments.append({
'PTID': '81',
'PTN': 'Stripe',
'PTNo1': pi.get("id") or '',
'PTNo5': card.get("last4", ""),
'PTNo1': p.info_data.get("id") or '',
'PTNo5': src.get("card", {}).get("last4") or '',
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo10': details.get('owner', {}).get('verified_name') or details.get('owner', {}).get('name') or '',
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
'PTNo15': p.full_id or '',
})
else:

View File

@@ -86,7 +86,6 @@ class InvoiceExporterMixin:
('', _('All payment providers')),
] + [
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
if not v.is_meta
],
required=False,
help_text=_('Only include invoices for orders that have at least one payment attempt '

View File

@@ -32,7 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict, defaultdict
from collections import OrderedDict
from decimal import Decimal
from zoneinfo import ZoneInfo
@@ -209,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
return qs.annotate(**annotations).filter(**filters)
return qs
def orders_qs(self, form_data):
def iterate_orders(self, form_data: dict):
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -250,15 +250,11 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
return qs
def iterate_orders(self, form_data: dict):
qs = self.orders_qs(form_data)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Event slug'), _('Event name'), _('Order code'), _('Order total'), _('Status'), _('Email'),
_('Phone number'), _('Order date'), _('Order time'), _('Company'), _('Name'),
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'),
_('Order date'), _('Order time'), _('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
@@ -335,7 +331,6 @@ class OrderListExporter(MultiSheetListExporter):
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
order.total,
order.get_extended_status_display(),
@@ -411,7 +406,7 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def fees_qs(self, form_data):
def iterate_fees(self, form_data: dict):
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -430,14 +425,9 @@ class OrderListExporter(MultiSheetListExporter):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_fees(self, form_data: dict):
qs = self.fees_qs(form_data)
headers = [
_('Event slug'),
_('Event name'),
_('Order code'),
_('Status'),
_('Email'),
@@ -474,7 +464,6 @@ class OrderListExporter(MultiSheetListExporter):
tz = ZoneInfo(order.event.settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
@@ -517,19 +506,7 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def positions_qs(self, form_data: dict):
qs = OrderPosition.all.filter(
order__event__in=self.events,
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_positions(self, form_data: dict):
base_qs = self.positions_qs(form_data)
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -539,6 +516,9 @@ class OrderListExporter(MultiSheetListExporter):
).values(
'm'
).order_by()
base_qs = OrderPosition.all.filter(
order__event__in=self.events,
)
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related(
@@ -548,12 +528,15 @@ class OrderListExporter(MultiSheetListExporter):
'subevent', 'subevent__meta_values',
'answers', 'answers__question', 'answers__options'
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
has_subevents = self.events.filter(has_subevents=True).exists()
headers = [
_('Event slug'),
_('Event name'),
_('Order code'),
_('Position ID'),
_('Status'),
@@ -605,9 +588,10 @@ class OrderListExporter(MultiSheetListExporter):
]
questions = list(Question.objects.filter(event__in=self.events))
options = defaultdict(list)
options = {}
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
options[q.pk] = []
if form_data['group_multiple_choice']:
for o in q.options.all():
options[q.pk].append(o)
@@ -617,9 +601,6 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(str(q.question) + ' ' + str(o.answer))
options[q.pk].append(o)
else:
if q.type == Question.TYPE_CHOICE:
for o in q.options.all():
options[q.pk].append(o)
headers.append(str(q.question))
headers += [
_('Company'),
@@ -657,7 +638,6 @@ class OrderListExporter(MultiSheetListExporter):
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
op.positionid,
_("canceled") if op.canceled else order.get_extended_status_display(),
@@ -729,7 +709,7 @@ class OrderListExporter(MultiSheetListExporter):
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
acache[a.question_id] = set(o.pk for o in a.options.all())
elif a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
@@ -742,10 +722,6 @@ class OrderListExporter(MultiSheetListExporter):
else:
for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
elif q.type == Question.TYPE_CHOICE:
# Join is only necessary if the question type was modified but also keeps the code simpler here
# as we'd otherwise need some [0] and existance checks
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
else:
row.append(acache.get(q.pk, ''))

View File

@@ -1,78 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..models import ReusableMedium
from ..signals import register_multievent_data_exporters
class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'reusablemedia'
verbose_name = _('Reusable media')
category = pgettext_lazy('export_category', 'Reusable media')
description = _('Download a spread sheet with the data of all reusable medias on your account.')
def iterate_list(self, form_data):
media = ReusableMedium.objects.filter(
organizer=self.organizer,
).select_related(
'customer', 'linked_orderposition', 'linked_giftcard',
).order_by('created')
headers = [
pgettext('reusable_medium', 'Media type'),
pgettext('reusable_medium', 'Identifier'),
_('Active'),
_('Expiration date'),
_('Customer account'),
_('Linked ticket'),
_('Linked gift card'),
_('Notes'),
]
yield headers
yield self.ProgressSetTotal(total=media.count())
for medium in media.iterator(chunk_size=1000):
row = [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_reusablemedia")
def register_multievent_i_reusable_media_exporter(sender, **kwargs):
return ReusableMediaExporter

View File

@@ -39,7 +39,6 @@ from django import forms
from django.core.validators import URLValidator
from django.forms.models import ModelFormMetaclass
from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
@@ -86,43 +85,6 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
class MarkdownTextarea(forms.Textarea):
def _render(self, template_name, context, renderer=None):
return mark_safe(
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
super()._render(template_name, context, renderer=None),
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
)
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
SECRET_REDACTED = '*****'

View File

@@ -35,7 +35,6 @@
import hashlib
import ipaddress
import logging
from django import forms
from django.conf import settings
@@ -45,13 +44,10 @@ from django.contrib.auth.password_validation import (
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix.base.metrics import pretix_failed_logins
from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
from pretix.helpers.http import get_client_ip
logger = logging.getLogger(__name__)
class LoginForm(forms.Form):
"""
@@ -59,7 +55,6 @@ class LoginForm(forms.Form):
username/password logins.
"""
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
origin = forms.CharField(widget=forms.HiddenInput, required=False)
error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."),
@@ -109,16 +104,12 @@ class LoginForm(forms.Form):
rc = get_redis_connection("redis")
cnt = rc.get(self.ratelimit_key)
if cnt and int(cnt) > 10:
pretix_failed_logins.inc(1, reason="ratelimit")
logger.info("Backend login rejected due to rate limit.")
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if self.user_cache is None:
if self.ratelimit_key:
rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 300)
logger.info("Backend login invalid.")
pretix_failed_logins.inc(1, reason="invalid")
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'
@@ -140,8 +131,6 @@ class LoginForm(forms.Form):
If the given user may log in, this method should return None.
"""
if not user.is_active:
logger.info("Backend login rejected due to user inactive.")
pretix_failed_logins.inc(1, reason="inactive")
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',

View File

@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -86,7 +86,6 @@ from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
)
@@ -126,7 +125,7 @@ class NamePartsWidget(forms.MultiWidget):
if fname == 'title' and self.titles:
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
elif fname == 'salutation':
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
widgets.append(Select(attrs=a, choices=[('', '---')] + PERSON_NAME_SALUTATIONS))
else:
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
@@ -137,10 +136,7 @@ class NamePartsWidget(forms.MultiWidget):
data = []
for i, field in enumerate(self.scheme['fields']):
fname, label, size = field
fval = value.get(fname, "")
if fname == "salutation" and fname in value and fval == "":
fval = "empty"
data.append(fval)
data.append(value.get(fname, ""))
if '_legacy' in value and not data[-1]:
data[-1] = value.get('_legacy', '')
elif not any(d for d in data) and '_scheme' in value:
@@ -194,8 +190,7 @@ class NamePartsFormField(forms.MultiValueField):
data = {}
data['_scheme'] = self.scheme_name
for i, value in enumerate(data_list):
key = self.scheme['fields'][i][0]
data[key] = value or ''
data[self.scheme['fields'][i][0]] = value or ''
return data
def __init__(self, *args, **kwargs):
@@ -244,7 +239,7 @@ class NamePartsFormField(forms.MultiValueField):
d.pop('validators', None)
field = forms.ChoiceField(
**d,
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
choices=[('', '---')] + PERSON_NAME_SALUTATIONS
)
else:
field = forms.CharField(**defaults)
@@ -270,9 +265,6 @@ class NamePartsFormField(forms.MultiValueField):
if sum(len(v) for v in value.values() if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":
value["salutation"] = ""
return value
@@ -607,41 +599,30 @@ class BaseQuestionsForm(forms.Form):
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
if item.validity_dynamic_start_choice_day_limit:
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
else:
max_date = None
min_date = time_machine_now()
initial = None
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
if pos.used_membership.date_start >= time_machine_now():
initial = min_date = pos.used_membership.date_start
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
attrs = {}
if max_date:
attrs['data-max'] = max_date.date().isoformat()
if min_date:
attrs['data-min'] = min_date.date().isoformat()
self.fields['requested_valid_from'] = forms.DateField(
label=_('Start date'),
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
widget=DatePickerWidget(attrs),
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
validators=[MaxDateValidator(max_date.date())] if max_date else []
)
else:
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
label=_('Start date'),
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
widget=SplitDateTimePickerWidget(
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=min_date,
max_date=max_date
),
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
validators=[MaxDateTimeValidator(max_date)] if max_date else []
)
add_fields = {}
@@ -1035,7 +1016,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.all_optional = kwargs.pop('all_optional', False)
kwargs.setdefault('initial', {})
if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
if not kwargs.get('instance') or not kwargs['instance'].country:
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
super().__init__(*args, **kwargs)
@@ -1171,7 +1152,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.vat_id_validated = False
messages.warning(self.request, e.message)
else:
raise ValidationError({"vat_id": e.message})
raise ValidationError(e.message)
except VATIDTemporaryError as e:
self.instance.vat_id_validated = False
if self.request and self.vat_warning:

View File

@@ -0,0 +1,63 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from bootstrap3.renderers import (
FieldRenderer as BaseFieldRenderer,
InlineFieldRenderer as BaseInlineFieldRenderer,
)
from django.forms import (
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
SelectDateWidget,
)
class FieldRenderer(BaseFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
class InlineFieldRenderer(BaseInlineFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html

View File

@@ -32,13 +32,13 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import re
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretix.helpers.format import format_map
class PlaceholderValidator(BaseValidator):
"""
@@ -47,12 +47,6 @@ class PlaceholderValidator(BaseValidator):
which are not presented in taken list.
"""
error_message = _(
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
'brackets on your placeholders match up. '
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
)
def __init__(self, limit_value):
super().__init__(limit_value)
self.limit_value = limit_value
@@ -63,15 +57,22 @@ class PlaceholderValidator(BaseValidator):
self.__call__(v)
return
try:
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
except ValueError:
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
except KeyError as e:
if value.count('{') != value.count('}'):
raise ValidationError(
_('Invalid placeholder: {%(value)s}'),
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
code='invalid_placeholder_syntax',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
invalid_placeholders = []
for placeholder in data_placeholders:
if placeholder not in self.limit_value:
invalid_placeholders.append(placeholder)
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid_placeholders',
params={'value': e.args[0]})
params={'value': ", ".join(invalid_placeholders,)})
def clean(self, x):
return x

View File

@@ -33,7 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import os
from datetime import datetime
from datetime import date
from django import forms
from django.utils.formats import get_format
@@ -188,11 +188,11 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs['autocomplete'] = 'off'
if min_date:
date_attrs['data-min'] = (
min_date if not isinstance(min_date, datetime) else min_date.astimezone(get_current_timezone()).date()
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
).isoformat()
if max_date:
date_attrs['data-max'] = (
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
).isoformat()
def date_placeholder():
@@ -209,10 +209,7 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
date_attrs['placeholder'] = lazy(date_placeholder, str)
time_attrs['placeholder'] = lazy(time_placeholder, str)
date_attrs['aria-label'] = _('Date')
time_attrs['aria-label'] = _('Time')
if 'aria-label' in attrs:
del attrs['aria-label']
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),
forms.TimeInput(attrs=time_attrs, format=time_format),

View File

@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
for family, styles in get_fonts().items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
@@ -625,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
)]
else:
tdata = [(
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]
@@ -855,7 +855,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = gettext_lazy('Default invoice renderer (European-style letter)')
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm
@@ -989,37 +989,6 @@ class Modern1Renderer(ClassicInvoiceRenderer):
canvas.drawText(textobject)
class Modern1SimplifiedRenderer(Modern1Renderer):
identifier = 'modern1simplified'
verbose_name = gettext_lazy('Simplified invoice renderer')
logo_left = Modern1Renderer.left_margin
logo_width = pagesizes.A4[0] - Modern1Renderer.right_margin - logo_left
logo_height = 25 * mm
logo_top = 13 * mm
logo_anchor = 'nw'
def _draw_invoice_from(self, canvas):
super(Modern1Renderer, self)._draw_invoice_from(canvas)
def _draw_event(self, canvas):
pass
def _get_intro(self):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(Paragraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
self.stylesheet['Normal'],
))
i.append(Spacer(2 * mm, 2 * mm))
return i + super()._get_intro()
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return [ClassicInvoiceRenderer, Modern1Renderer, Modern1SimplifiedRenderer]
return [ClassicInvoiceRenderer, Modern1Renderer]

View File

@@ -268,10 +268,7 @@ def metric_values():
dkey = key.decode("utf-8")
splitted = dkey.split("{", 2)
value = float(value.decode("utf-8"))
if len(splitted) == 1:
metrics[splitted[0]][""] = value
else:
metrics[splitted[0]]["{" + splitted[1]] = value
metrics[splitted[0]]["{" + splitted[1]] = value
# Aliases
aliases = {
@@ -317,5 +314,3 @@ pretix_task_runs_total = Counter("pretix_task_runs_total", "Total calls to a cel
["task_name", "status"])
pretix_task_duration_seconds = Histogram("pretix_task_duration_seconds", "Call time of a celery task",
["task_name"])
pretix_successful_logins = Counter("pretix_logins_successful", "Successful logins", [])
pretix_failed_logins = Counter("pretix_logins_failed", "Failed logins", ["reason"])

View File

@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
@@ -40,30 +40,10 @@ from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
from pretix.presale.style import get_fonts
_supported = None
def get_supported_language(requested_language, allowed_languages, default_language):
language = requested_language
if language not in allowed_languages:
firstpart = language.split('-')[0]
if firstpart in allowed_languages:
language = firstpart
else:
language = default_language
for lang in allowed_languages:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in allowed_languages:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = allowed_languages[0]
return language
class LocaleMiddleware(MiddlewareMixin):
"""
@@ -85,11 +65,20 @@ class LocaleMiddleware(MiddlewareMixin):
settings_holder = None
if settings_holder:
language = get_supported_language(
language,
settings_holder.settings.locales,
settings_holder.settings.locale,
)
if language not in settings_holder.settings.locales:
firstpart = language.split('-')[0]
if firstpart in settings_holder.settings.locales:
language = firstpart
else:
language = settings_holder.settings.locale
for lang in settings_holder.settings.locales:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in settings_holder.settings.locales:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = settings_holder.settings.locales[0]
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
else:
@@ -241,14 +230,6 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
@@ -268,14 +249,6 @@ class SecurityMiddleware(MiddlewareMixin):
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
'script-src': ['{static}'],
@@ -284,7 +257,7 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"] + list(font_src),
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or

View File

@@ -1,28 +0,0 @@
# Generated by Django 4.2.4 on 2023-12-06 14:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0253_checkin_info"),
]
operations = [
migrations.AlterField(
model_name="logentry",
name="organizer_link",
field=models.ForeignKey(
db_column="organizer_link_id",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.organizer",
),
),
migrations.RenameField(
model_name="logentry",
old_name="organizer_link",
new_name="organizer",
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.2.4 on 2023-11-22 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
]
operations = [
migrations.AddField(
model_name="item",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="item",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
)
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.2.4 on 2024-01-11 15:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0255_item_unavail_modes"),
]
operations = [
migrations.AddField(
model_name="itemvariation",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="itemvariation",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.2.9 on 2024-01-30 11:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0256_itemvariation_unavail_modes"),
]
operations = [
migrations.AlterField(
model_name="item",
name="default_price",
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
preserve_default=False,
),
]

View File

@@ -1,48 +0,0 @@
# Generated by Django 4.2.10 on 2024-03-15 09:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0257_item_default_price_not_null"),
]
operations = [
migrations.AddField(
model_name="order",
name="organizer",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="pretixbase.organizer",
),
),
migrations.AddField(
model_name="orderposition",
name="organizer",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="order_positions",
to="pretixbase.organizer",
),
),
migrations.AddConstraint(
model_name="order",
constraint=models.UniqueConstraint(
fields=("organizer", "code"), name="order_organizer_code_uniq"
),
),
migrations.AddConstraint(
model_name="orderposition",
constraint=models.UniqueConstraint(
models.F("organizer"),
models.F("secret"),
name="orderposition_organizer_secret_uniq",
),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-02 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0258_uniq_indx"),
]
operations = [
migrations.AddField(
model_name="team",
name="require_2fa",
field=models.BooleanField(default=False),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0259_team_require_2fa"),
]
operations = [
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
),
]

View File

@@ -1,48 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-02 15:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0260_alter_reusablemedium_index_together"),
]
operations = [
migrations.CreateModel(
name="UserKnownLoginSource",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("agent_type", models.CharField(max_length=255, null=True)),
("device_type", models.CharField(max_length=255, null=True)),
("os_type", models.CharField(max_length=255, null=True)),
(
"country",
pretix.helpers.countries.FastCountryField(
countries=pretix.helpers.countries.CachedCountries,
max_length=2,
null=True,
),
),
("last_seen", models.DateTimeField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="known_login_sources",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-19 14:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0261_userknownloginsource"),
]
operations = [
migrations.AddField(
model_name="subevent",
name="comment",
field=models.TextField(null=True),
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-09 07:32
from django.db import migrations
def change_currencies(apps, schema_editor):
Event = apps.get_model("pretixbase", "Event")
Event.objects.filter(
currency__in={
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
).update(currency='XXX')
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0262_subevent_comment"),
]
operations = [
migrations.RunPython(
change_currencies, migrations.RunPython.noop
)
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 4.2.11 on 2024-05-16 11:07
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0263_auto_20240409_0732"),
]
operations = [
migrations.AddField(
model_name="order",
name="internal_secret",
field=models.CharField(
default=None,
max_length=32,
null=True,
),
),
]

View File

@@ -1,287 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import csv
import datetime
import io
import re
from decimal import Decimal, DecimalException
from django.core.exceptions import ValidationError
from django.core.validators import validate_integer
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.i18n import LazyLocaleException
from pretix.base.models import SubEvent
class DataImportError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def parse_csv(file, length=None, mode="strict", charset=None):
file.seek(0)
data = file.read(length)
if not charset:
try:
import chardet
charset = chardet.detect(data)['encoding']
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
try:
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
except csv.Error:
return None
if dialect is None:
return None
reader = csv.DictReader(io.StringIO(data), dialect=dialect)
return reader
class ImportColumn:
@property
def identifier(self):
"""
Unique, internal name of the column.
"""
raise NotImplementedError
@property
def verbose_name(self):
"""
Human-readable description of the column
"""
raise NotImplementedError
@property
def initial(self):
"""
Initial value for the form component
"""
return None
@property
def default_value(self):
"""
Internal default value for the assignment of this column. Defaults to ``empty``. Return ``None`` to disable this
option.
"""
return 'empty'
@property
def default_label(self):
"""
Human-readable description of the default assignment of this column, defaults to "Keep empty".
"""
return gettext_lazy('Keep empty')
def __init__(self, event):
self.event = event
def static_choices(self):
"""
This will be called when rendering the form component and allows you to return a list of values that can be
selected by the user statically during import.
:return: list of 2-tuples of strings
"""
return []
def resolve(self, settings, record):
"""
This method will be called to get the raw value for this field, usually by either using a static value or
inspecting the CSV file for the assigned header. You usually do not need to implement this on your own,
the default should be fine.
"""
k = settings.get(self.identifier, self.default_value)
if k == self.default_value:
return None
elif k.startswith('csv:'):
return record.get(k[4:], None) or None
elif k.startswith('static:'):
return k[7:]
raise ValidationError(_('Invalid setting for column "{header}".').format(header=self.verbose_name))
def clean(self, value, previous_values):
"""
Allows you to validate the raw input value for your column. Raise ``ValidationError`` if the value is invalid.
You do not need to include the column or row name or value in the error message as it will automatically be
included.
:param value: Contains the raw value of your column as returned by ``resolve``. This can usually be ``None``,
e.g. if the column is empty or does not exist in this row.
:param previous_values: Dictionary containing the validated values of all columns that have already been validated.
"""
return value
def assign(self, value, obj, **kwargs):
"""
This will be called to perform the actual import. You are supposed to set attributes on the ``obj`` or other
related objects that get passed in based on the input ``value``. This is called *before* the actual database
transaction, so the input objects do not yet have a primary key. If you want to create related objects, you
need to place them into some sort of internal queue and persist them when ``save`` is called.
"""
pass
def save(self, obj):
"""
This will be called to perform the actual import. This is called inside the actual database transaction and the
input object ``obj`` has already been saved to the database.
"""
pass
@property
def timezone(self):
return self.event.timezone
def i18n_flat(l):
if isinstance(l.data, dict):
return l.data.values()
return [l.data]
class BooleanColumnMixin:
default_value = None
initial = "static:false"
def static_choices(self):
return (
("false", _("No")),
("true", _("Yes")),
)
def clean(self, value, previous_values):
if not value:
return False
if value.lower() in ("true", "1", "yes", _("Yes").lower()):
return True
elif value.lower() in ("false", "0", "no", _("No").lower()):
return False
else:
raise ValidationError(_("Could not parse {value} as a yes/no value.").format(value=value))
class DatetimeColumnMixin:
def clean(self, value, previous_values):
if not value:
return
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.timezone)
return d
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
class DecimalColumnMixin:
def clean(self, value, previous_values):
if value not in (None, ''):
value = formats.sanitize_separators(re.sub(r'[^0-9.,-]', '', value))
try:
value = Decimal(value)
except (DecimalException, TypeError):
raise ValidationError(_('You entered an invalid number.'))
return value
class IntegerColumnMixin:
def clean(self, value, previous_values):
if value is not None:
validate_integer(value)
return int(value)
class SubeventColumnMixin:
def __init__(self, *args, **kwargs):
self._subevent_cache = {}
super().__init__(*args, **kwargs)
@cached_property
def subevents(self):
return list(self.event.subevents.filter(active=True).order_by('date_from'))
def static_choices(self):
return [
(str(p.pk), str(p)) for p in self.subevents
]
def clean(self, value, previous_values):
if value in self._subevent_cache:
return self._subevent_cache[value]
input_formats = formats.get_format('DATETIME_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.event.timezone)
try:
se = self.event.subevents.get(
active=True,
date_from__gt=d - datetime.timedelta(seconds=1),
date_from__lt=d + datetime.timedelta(seconds=1),
)
self._subevent_cache[value] = se
return se
except SubEvent.DoesNotExist:
raise ValidationError(pgettext("subevent", "No matching date was found."))
except SubEvent.MultipleObjectsReturned:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
except (ValueError, TypeError):
continue
matches = [
p for p in self.subevents
if str(p.pk) == value or any(
(v and v == value) for v in i18n_flat(p.name)) or p.date_from.isoformat() == value
]
if len(matches) == 0:
raise ValidationError(pgettext("subevent", "No matching date was found."))
if len(matches) > 1:
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
self._subevent_cache[value] = matches[0]
return matches[0]

View File

@@ -1,385 +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 decimal import Decimal
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pretix.base.modelimport import (
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
IntegerColumnMixin, i18n_flat,
)
from pretix.base.models import ItemVariation, Quota, Seat, Voucher
from pretix.base.signals import voucher_import_columns
class CodeColumn(ImportColumn):
identifier = 'code'
verbose_name = gettext_lazy('Voucher code')
default_value = None
def __init__(self, *args):
self._cached = set()
super().__init__(*args)
def clean(self, value, previous_values):
if value:
MinLengthValidator(5)(value)
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
raise ValidationError(_('A voucher with this code already exists.'))
self._cached.add(value)
return value
def assign(self, value, obj: Voucher, **kwargs):
obj.code = value
class SubeventColumn(ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
def assign(self, value, obj: Voucher, **kwargs):
obj.subevent = value
class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'max_usages'
verbose_name = gettext_lazy('Maximum usages')
default_value = None
initial = "static:1"
def static_choices(self):
return [
("1", "1")
]
def clean(self, value, previous_values):
if value is None and previous_values.get("code"):
raise ValidationError(_('The maximum number of usages must be set.'))
return super().clean(value, previous_values)
def assign(self, value, obj: Voucher, **kwargs):
obj.max_usages = value if value is not None else 1
class MinUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'min_usages'
verbose_name = gettext_lazy('Minimum usages')
default_value = None
initial = "static:1"
def static_choices(self):
return [
("1", "1")
]
def assign(self, value, obj: Voucher, **kwargs):
obj.min_usages = value if value is not None else 1
class BudgetColumn(DecimalColumnMixin, ImportColumn):
identifier = 'budget'
verbose_name = gettext_lazy('Maximum discount budget')
def assign(self, value, obj: Voucher, **kwargs):
obj.budget = value
class ValidUntilColumn(DatetimeColumnMixin, ImportColumn):
identifier = 'valid_until'
verbose_name = gettext_lazy('Valid until')
def assign(self, value, obj: Voucher, **kwargs):
obj.valid_until = value
class BlockQuotaColumn(BooleanColumnMixin, ImportColumn):
identifier = 'block_quota'
verbose_name = gettext_lazy('Reserve ticket from quota')
def assign(self, value, obj: Voucher, **kwargs):
obj.block_quota = value
class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
identifier = 'allow_ignore_quota'
verbose_name = gettext_lazy('Allow to bypass quota')
def assign(self, value, obj: Voucher, **kwargs):
obj.allow_ignore_quota = value
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price mode')
default_value = None
initial = 'static:none'
def static_choices(self):
return Voucher.PRICE_MODES
def clean(self, value, previous_values):
d = dict(Voucher.PRICE_MODES)
reverse = {v: k for k, v in Voucher.PRICE_MODES}
if value in d:
return value
elif value in reverse:
return reverse[value]
else:
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
value=value, options=', '.join(d.keys())
))
def assign(self, value, voucher: Voucher, **kwargs):
voucher.price_mode = value
class ValueColumn(DecimalColumnMixin, ImportColumn):
identifier = 'value'
verbose_name = gettext_lazy('Voucher value')
def clean(self, value, previous_values):
value = super().clean(value, previous_values)
if value and previous_values.get("price_mode") == "none":
raise ValidationError(_("It is pointless to set a value without a price mode."))
return value
def assign(self, value, obj: Voucher, **kwargs):
obj.value = value or Decimal("0.00")
class ItemColumn(ImportColumn):
identifier = 'item'
verbose_name = gettext_lazy('Product')
@cached_property
def items(self):
return list(self.event.items.filter(active=True))
def static_choices(self):
return [
(str(p.pk), str(p)) for p in self.items
]
def clean(self, value, previous_values):
if not value:
return
matches = [
p for p in self.items
if str(p.pk) == value or (p.internal_name and p.internal_name == value) or any(
(v and v == value) for v in i18n_flat(p.name))
]
if len(matches) == 0:
raise ValidationError(_("No matching product was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching products were found."))
return matches[0]
def assign(self, value, voucher, **kwargs):
voucher.item = value
class VariationColumn(ImportColumn):
identifier = 'variation'
verbose_name = gettext_lazy('Product variation')
@cached_property
def items(self):
return list(ItemVariation.objects.filter(
active=True, item__active=True, item__event=self.event
).select_related('item'))
def static_choices(self):
return [
(str(p.pk), '{} {}'.format(p.item, p.value)) for p in self.items
]
def clean(self, value, previous_values):
if value:
matches = [
p for p in self.items
if (str(p.pk) == value or any((v and v == value) for v in i18n_flat(p.value))) and p.item_id == previous_values['item'].pk
]
if len(matches) == 0:
raise ValidationError(_("No matching variation was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching variations were found."))
return matches[0]
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.variation = value
class QuotaColumn(ImportColumn):
identifier = 'quota'
verbose_name = gettext_lazy('Quota')
@cached_property
def quotas(self):
return list(Quota.objects.filter(
event=self.event
))
def static_choices(self):
return [
(str(q.pk), q.name) for q in self.quotas
]
def clean(self, value, previous_values):
if value:
if previous_values.get('item'):
raise ValidationError(_("You cannot specify a quota if you specified a product."))
matches = [
q for q in self.quotas
if str(q.pk) == value or q.name == value
]
if len(matches) == 0:
raise ValidationError(_("No matching variation was found."))
if len(matches) > 1:
raise ValidationError(_("Multiple matching variations were found."))
return matches[0]
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.quota = value
class SeatColumn(ImportColumn):
identifier = 'seat'
verbose_name = gettext_lazy('Seat ID')
def __init__(self, *args):
self._cached = set()
super().__init__(*args)
def clean(self, value, previous_values):
if value:
if self.event.has_subevents:
if not previous_values.get('subevent'):
raise ValidationError(_('You need to choose a date if you select a seat.'))
try:
value = Seat.objects.get(
event=self.event,
seat_guid=value,
subevent=previous_values.get('subevent')
)
except Seat.MultipleObjectsReturned:
raise ValidationError(_('Multiple matching seats were found.'))
except Seat.DoesNotExist:
raise ValidationError(_('No matching seat was found.'))
if not value.is_available() or value in self._cached:
raise ValidationError(
_('The seat you selected has already been taken. Please select a different seat.'))
if previous_values.get("quota"):
raise ValidationError(_('You need to choose a specific product if you select a seat.'))
if previous_values.get('max_usages', 1) > 1 or previous_values.get('min_usages', 1) > 1:
raise ValidationError(_('Seat-specific vouchers can only be used once.'))
if previous_values.get("item") and value.product != previous_values.get("item"):
raise ValidationError(
_('You need to choose the product "{prod}" for this seat.').format(prod=value.product)
)
self._cached.add(value)
return value
def assign(self, value, voucher: Voucher, **kwargs):
voucher.seat = value
class TagColumn(ImportColumn):
identifier = 'tag'
verbose_name = gettext_lazy('Tag')
def assign(self, value, voucher: Voucher, **kwargs):
voucher.tag = value or ''
class CommentColumn(ImportColumn):
identifier = 'comment'
verbose_name = gettext_lazy('Comment')
def assign(self, value, voucher: Voucher, **kwargs):
voucher.comment = value or ''
class ShowHiddenItemsColumn(BooleanColumnMixin, ImportColumn):
identifier = 'show_hidden_items'
verbose_name = gettext_lazy('Shows hidden products that match this voucher')
initial = "static:true"
def assign(self, value, obj: Voucher, **kwargs):
obj.show_hidden_items = value
class AllAddonsIncludedColumn(BooleanColumnMixin, ImportColumn):
identifier = 'all_addons_included'
verbose_name = gettext_lazy('Offer all add-on products for free when redeeming this voucher')
def assign(self, value, obj: Voucher, **kwargs):
obj.all_addons_included = value
class AllBundlesIncludedColumn(BooleanColumnMixin, ImportColumn):
identifier = 'all_bundles_included'
verbose_name = gettext_lazy('Include all bundled products without a designated price when redeeming this voucher')
def assign(self, value, obj: Voucher, **kwargs):
obj.all_bundles_included = value
def get_voucher_import_columns(event):
default = []
if event.has_subevents:
default.append(SubeventColumn(event))
default += [
CodeColumn(event),
MaxUsagesColumn(event),
MinUsagesColumn(event),
BudgetColumn(event),
ValidUntilColumn(event),
BlockQuotaColumn(event),
AllowIgnoreQuotaColumn(event),
PriceModeColumn(event),
ValueColumn(event),
ItemColumn(event),
VariationColumn(event),
QuotaColumn(event),
SeatColumn(event),
TagColumn(event),
CommentColumn(event),
ShowHiddenItemsColumn(event),
AllAddonsIncludedColumn(event),
AllBundlesIncludedColumn(event),
]
for recv, resp in voucher_import_columns.send(sender=event):
default += resp
return default

View File

@@ -37,7 +37,9 @@ import json
import operator
from datetime import timedelta
from functools import reduce
from urllib.parse import urlparse
import webauthn
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -51,13 +53,13 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
from ...helpers.countries import FastCountryField
from ...helpers.u2f import pub_key_from_der, websafe_decode
from .base import LoggingMixin
@@ -418,22 +420,18 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
else:
return set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this user is part of any team that grants access of type ``perm_name``
to the event ``event``.
Either ``request`` or ``session_key`` are required to detect staff sessions properly.
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: The current request (optional)
:param session_key: The current session key (optional)
:param request: The current request (optional). Required to detect staff sessions properly.
:return: bool
"""
assert not (session_key and request)
if (session_key or request) and self.has_active_staff_session(session_key or request.session.session_key):
if request and self.has_active_staff_session(request.session.session_key):
return True
teams = self._get_teams_for_event(organizer, event)
if teams:
@@ -584,15 +582,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
self.save(update_fields=['session_token'])
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True)
device_type = models.CharField(max_length=255, null=True, blank=True)
os_type = models.CharField(max_length=255, null=True, blank=True)
country = FastCountryField(null=True, blank=True)
last_seen = models.DateTimeField()
class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT)
date_start = models.DateTimeField(auto_now_add=True)
@@ -619,12 +608,7 @@ class U2FDevice(Device):
json_data = models.TextField()
@property
def webauthndevice(self):
d = json.loads(self.json_data)
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
@property
def webauthnpubkey(self):
def webauthnuser(self):
d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This
@@ -636,7 +620,16 @@ class U2FDevice(Device):
pub_key.public_numbers().x, pub_key.public_numbers().y
)
)
return pub_key
return webauthn.WebAuthnUser(
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device):
@@ -648,9 +641,14 @@ class WebAuthnDevice(Device):
sign_count = models.IntegerField(default=0)
@property
def webauthndevice(self):
return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
@property
def webauthnpubkey(self):
return websafe_decode(self.pub_key)
def webauthnuser(self):
return webauthn.WebAuthnUser(
self.ukey,
self.user.email,
str(self.user),
settings.SITE_URL,
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)

View File

@@ -115,7 +115,7 @@ class LoggingMixin:
kwargs['api_token'] = api_token
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
organizer_id=organizer_id, **kwargs)
organizer_link_id=organizer_id, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']

View File

@@ -280,12 +280,11 @@ class CheckinList(LoggedModel):
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
}
allowed_operators = top_level_operators | {
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
'entries_days_before',
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate', 'entry_status',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
}
if not rules or not isinstance(rules, dict):
return rules
@@ -310,7 +309,7 @@ class CheckinList(LoggedModel):
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
return rules
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
if operator in ('entries_since', 'entries_before'):
if len(values) != 1 or "buildTime" not in values[0]:
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')

View File

@@ -23,7 +23,6 @@
from collections import defaultdict
from decimal import Decimal
from itertools import groupby
from math import ceil
from typing import Dict, Optional, Tuple
from django.core.exceptions import ValidationError
@@ -273,7 +272,7 @@ class Discount(LoggedModel):
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group))
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
else:
@@ -345,7 +344,7 @@ class Discount(LoggedModel):
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
return positions[idx][1] or 0 # subevent_id
return positions[idx][1] # subevent_id
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group

View File

@@ -45,7 +45,6 @@ from zoneinfo import ZoneInfo
import pytz_deprecation_shim
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import (
@@ -68,7 +67,6 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.timemachine import time_machine_now
from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
@@ -231,25 +229,17 @@ class EventMixin:
else:
return self.presale_end
@property
def waiting_list_active(self):
if not self.settings.waiting_list_enabled:
return False
if self.settings.waiting_list_auto_disable:
return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now()
return True
@property
def presale_has_ended(self):
"""
Is true, when ``presale_end`` is set and in the past.
"""
if self.effective_presale_end:
return time_machine_now() > self.effective_presale_end
return now() > self.effective_presale_end
elif self.date_to:
return time_machine_now() > self.date_to
return now() > self.date_to
else:
return time_machine_now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property
def effective_presale_start(self):
@@ -269,15 +259,12 @@ class EventMixin:
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past.
"""
if self.effective_presale_start and time_machine_now() < self.effective_presale_start:
if self.effective_presale_start and now() < self.effective_presale_start:
return False
return not self.presale_has_ended
@property
def event_microdata(self):
if self.settings.event_microdata:
return self.settings.event_microdata
import json
eventdict = {
@@ -317,11 +304,11 @@ class EventMixin:
q_variation = (
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__require_bundling=False)
@@ -696,7 +683,7 @@ class Event(EventMixin, LoggedModel):
@property
def presale_has_ended(self):
if self.has_subevents:
return self.presale_end and time_machine_now() > self.presale_end
return self.presale_end and now() > self.presale_end
else:
return super().presale_has_ended
@@ -788,7 +775,7 @@ class Event(EventMixin, LoggedModel):
time(hour=23, minute=59, second=59)
), tz)
def copy_data_from(self, other, skip_meta_data=False):
def copy_data_from(self, other):
from pretix.presale.style import regenerate_css
from ..signals import event_copy_data
@@ -811,11 +798,10 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if not skip_meta_data:
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
emv.save(force_insert=True)
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
fl.pk = None
@@ -1027,7 +1013,7 @@ class Event(EventMixin, LoggedModel):
s.object = self
s.pk = None
if s.value.startswith('file://') and settings_hierarkey.get_declared_type(s.key) == File:
if s.value.startswith('file://'):
fi = default_storage.open(s.value[len('file://'):], 'rb')
nonce = get_random_string(length=8)
fname_base = clean_filename(os.path.basename(s.value))
@@ -1077,7 +1063,7 @@ class Event(EventMixin, LoggedModel):
providers[pp.identifier] = pp
self._cached_payment_providers = OrderedDict(sorted(
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name).title())
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
))
return self._cached_payment_providers
@@ -1189,8 +1175,8 @@ class Event(EventMixin, LoggedModel):
)
).filter(
Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=time_machine_now() - timedelta(hours=24)))
| Q(date_to__gte=time_machine_now() - timedelta(hours=24))
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
)
) # order_by doesn't make sense with I18nField
if ordering in ("date_ascending", "date_descending"):
@@ -1239,9 +1225,6 @@ class Event(EventMixin, LoggedModel):
if self.has_paid_things and not self.has_payment_provider:
issues.append(_('You have configured at least one paid product but have not enabled any payment methods.'))
if self.has_paid_things and self.currency == "XXX":
issues.append(_('You have configured at least one paid product but have not configured a currency.'))
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
@@ -1463,15 +1446,13 @@ class SubEvent(EventMixin, LoggedModel):
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text"),
verbose_name=_("Frontpage text")
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents', verbose_name=_('Seating plan'))
comment = models.TextField(
verbose_name=_("Internal comment"),
null=True, blank=True
)
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
last_modified = models.DateTimeField(
auto_now=True, db_index=True
@@ -1508,7 +1489,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_items=Coalesce(
Subquery(
SubEventItem.objects.filter(
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
output_field=models.TextField(),
@@ -1519,7 +1500,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_vars=Coalesce(
Subquery(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
output_field=models.TextField(),

View File

@@ -55,7 +55,7 @@ from django.db.models import Q
from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_scopes import ScopedManager
@@ -65,7 +65,6 @@ from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
@@ -193,7 +192,7 @@ class SubEventItem(models.Model):
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
@@ -249,7 +248,7 @@ class SubEventItemVariation(models.Model):
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
@@ -264,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
@@ -375,13 +374,6 @@ class Item(LoggedModel):
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
)
UNAVAIL_MODE_HIDDEN = "hide"
UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
)
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
@@ -431,7 +423,7 @@ class Item(LoggedModel):
help_text=_("If this product has multiple variations, you can set different prices for each of the "
"variations. If a variation does not have a special price or if you do not have variations, "
"this price will be used."),
max_digits=13, decimal_places=2,
max_digits=13, decimal_places=2, null=True
)
free_price = models.BooleanField(
default=False,
@@ -444,8 +436,7 @@ class Item(LoggedModel):
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option. This "
"will be ignored if a voucher is used that lowers the price."),
"value, but not lower than the price this product would have without the free price option."),
max_digits=13, decimal_places=2, null=True, blank=True,
)
tax_rule = models.ForeignKey(
@@ -496,21 +487,11 @@ class Item(LoggedModel):
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
hidden_if_available = models.ForeignKey(
'Quota',
null=True, blank=True,
@@ -650,7 +631,7 @@ class Item(LoggedModel):
null=True, blank=True, max_length=16,
verbose_name=_('Validity'),
help_text=_(
'When setting up a regular event, or an event series with time slots, you typically do NOT need to change '
'When setting up a regular event, or an event series with time slots, you typically to NOT need to change '
'this value. The default setting means that the validity time of tickets will not be decided by the '
'product, but by the event and check-in configuration. Only use the other options if you need them to '
'realize e.g. a booking of a year-long ticket with a dynamic start date. Note that the validity will be '
@@ -722,8 +703,6 @@ class Item(LoggedModel):
return str(self.internal_name or self.name)
def save(self, *args, **kwargs):
if self.hide_without_voucher:
self.require_voucher = True
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -784,7 +763,7 @@ class Item(LoggedModel):
return t
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
@@ -796,29 +775,11 @@ class Item(LoggedModel):
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or time_machine_now()
subevent_item = subevent and subevent.item_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
return 'require_voucher'
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
return 'available_from'
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
return 'available_until'
else:
return None
def _get_quotas(self, ignored_quotas=None, subevent=None):
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
@@ -959,11 +920,11 @@ class Item(LoggedModel):
return self.validity_fixed_from, self.validity_fixed_until
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
tz = override_tz or self.event.timezone
requested_start = requested_start or time_machine_now()
requested_start = requested_start or now()
if enforce_start_limit and not self.validity_dynamic_start_choice:
requested_start = time_machine_now()
requested_start = now()
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
valid_until = requested_start.astimezone(tz)
@@ -1087,8 +1048,7 @@ class ItemVariation(models.Model):
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option. This "
"will be ignored if a voucher is used that lowers the price."),
"value, but not lower than the price this product would have without the free price option."),
max_digits=13, decimal_places=2, null=True, blank=True,
)
require_approval = models.BooleanField(
@@ -1118,21 +1078,11 @@ class ItemVariation(models.Model):
null=True, blank=True,
help_text=_('This variation will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This variation will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
@@ -1293,7 +1243,7 @@ class ItemVariation(models.Model):
return ItemVariation.objects.filter(item=self.item).count() == 1
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
@@ -1305,27 +1255,11 @@ class ItemVariation(models.Model):
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or time_machine_now()
subevent_var = subevent and subevent.var_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
return 'available_from'
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
return 'available_until'
else:
return None
@property
def meta_data(self):
data = self.item.meta_data

View File

@@ -78,7 +78,7 @@ class LogEntry(models.Model):
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
organizer = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT, db_column='organizer_link_id')
organizer_link = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
visible = models.BooleanField(default=True)
@@ -123,6 +123,22 @@ class LogEntry(models.Model):
typepath = typepath.rsplit('.', 1)[0]
return no_type
@cached_property
def organizer(self):
from .organizer import Organizer
if self.organizer_link:
return self.organizer_link
elif self.event:
return self.event.organizer
elif hasattr(self.content_object, 'event'):
return self.content_object.event.organizer
elif hasattr(self.content_object, 'organizer'):
return self.content_object.organizer
elif isinstance(self.content_object, Organizer):
return self.content_object
return None
@cached_property
def display_object(self):
from . import (

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