forked from CGM_Public/pretix_original
Compare commits
2 Commits
fix-tests
...
fix-countr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8d06e78f | ||
|
|
3bde9d8b0e |
@@ -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
|
||||
|
||||
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
|
||||
fi
|
||||
|
||||
if [ "$1" == "upgrade" ]; then
|
||||
exec python3 -m pretix updateassets
|
||||
exec python3 -m pretix updatestyles
|
||||
fi
|
||||
|
||||
exec python3 -m pretix "$@"
|
||||
|
||||
@@ -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
|
||||
@@ -19,7 +19,7 @@ You can use ``pip`` to update pretix directly to the development branch. Then, u
|
||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updateassets
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Docker installation
|
||||
|
||||
@@ -14,4 +14,3 @@ for your needs.
|
||||
manual_smallscale
|
||||
dev_version
|
||||
enterprise
|
||||
community
|
||||
|
||||
@@ -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,12 +279,11 @@ 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
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updateassets
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
@@ -325,7 +323,7 @@ Then, proceed like after any plugin installation::
|
||||
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updateassets
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,11 +40,6 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat (or ``null``)
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -20,12 +20,8 @@ id integer Internal ID
|
||||
active boolean The discount will be ignored if this is ``false``
|
||||
internal_name string A name for the rule used in the backend
|
||||
position integer An integer, used for sorting the rules which are applied in order
|
||||
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||
that support discounts.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
sales_channels list of strings Sales channels this discount is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this discount can be applied
|
||||
@@ -99,8 +95,6 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -157,8 +151,6 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -201,8 +193,6 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -234,8 +224,6 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -296,8 +284,6 @@ Endpoints
|
||||
"active": false,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
|
||||
@@ -49,11 +49,8 @@ item_meta_properties object Item-specific m
|
||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||
For performance reason, value is omitted in lists and
|
||||
only contained in detail views. Value can be cached.
|
||||
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the event is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
sales_channels list A list of sales channels this event is available for
|
||||
sale on.
|
||||
public_url string The public, customer-facing URL of the event (read-only).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -134,13 +131,11 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": [
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
],
|
||||
"sales_channels": [],
|
||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||
}
|
||||
]
|
||||
@@ -230,8 +225,6 @@ Endpoints
|
||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||
]
|
||||
},
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -289,8 +282,11 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -326,8 +322,6 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -393,8 +387,11 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -430,8 +427,6 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -507,8 +502,6 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.pretixdroid"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
|
||||
@@ -96,8 +96,6 @@ Endpoints
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string secret: Only show gift cards with the given secret.
|
||||
:query string value: Only show gift cards with the given value.
|
||||
:query boolean expired: Filter for gift cards that are (not) expired.
|
||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||
|
||||
@@ -30,7 +30,6 @@ at :ref:`plugin-docs`.
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
saleschannels
|
||||
membershiptypes
|
||||
memberships
|
||||
giftcards
|
||||
|
||||
@@ -38,14 +38,11 @@ require_membership boolean If ``true``, bo
|
||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||
be hidden from users without a valid membership.
|
||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
channel needs to be on both lists for the item to be
|
||||
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
|
||||
@@ -114,8 +111,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -144,8 +139,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -164,7 +157,6 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by the value of the variation (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -210,8 +202,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -254,8 +244,7 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -288,8 +277,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -354,8 +341,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
|
||||
@@ -46,11 +46,8 @@ personalized boolean ``true`` for
|
||||
position integer An integer, used for sorting
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the item is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"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
|
||||
@@ -160,14 +157,11 @@ variations list of objects A list with o
|
||||
be hidden from users without a valid membership.
|
||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
Markdown syntax or can be ``null``.
|
||||
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
channel needs to be on both lists for the item to be
|
||||
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
|
||||
@@ -282,8 +276,6 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -348,8 +340,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -372,8 +362,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -392,7 +380,6 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by internal name or name of the item (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||
@@ -433,8 +420,6 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -500,8 +485,6 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"description": null,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -523,8 +506,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -564,8 +545,7 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
@@ -628,8 +608,7 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -651,8 +630,7 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -679,8 +657,6 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -745,8 +721,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -769,8 +743,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -829,8 +801,6 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "25.00",
|
||||
"original_price": null,
|
||||
@@ -895,8 +865,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -919,8 +887,6 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
|
||||
@@ -215,11 +215,6 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
@@ -460,13 +455,10 @@ List of all orders
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
you will not notice it using this method.
|
||||
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
|
||||
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
|
||||
:query datetime created_since: Only return orders that have been created since the given date.
|
||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
|
||||
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
|
||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
Sales channels
|
||||
==============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The sales channel resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the sales channel. For sales channel types
|
||||
that allow only one instance, this is the same as ``type``.
|
||||
For sales channel types that allow multiple instances, this
|
||||
is always prefixed with ``type.``.
|
||||
label multi-lingual string Human-readable name of the sales channel
|
||||
type string Type of the sales channel. Only channels with type ``api``
|
||||
can currently be created through the API.
|
||||
position integer Position for sorting lists of sales channels
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
|
||||
|
||||
Returns a list of all sales channels within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Returns information on one sales channel, identified by its identifier.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param identifier: The ``identifier`` field of the sales channel to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/saleschannels/
|
||||
|
||||
Creates a sales channel
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a sales channel for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The sales channel could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Update a sales channel. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"position": 5
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 5
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param identifier: The ``identifier`` field of the sales channel to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The sales channel could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
|
||||
of the system.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param identifier: The ``identifier`` field of the sales channel to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the sales channel is currently in use.
|
||||
@@ -12,7 +12,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_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
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
|
||||
|
||||
@@ -35,7 +35,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
|
||||
create and manage their events, items, orders and tickets.
|
||||
|
||||
**presale**
|
||||
This is the ticket shop itself, containing all of the parts visible to the
|
||||
This is the ticket-shop itself, containing all of the parts visible to the
|
||||
end user. Also called "frontend" in parts of this documentation.
|
||||
|
||||
**api**
|
||||
|
||||
@@ -19,4 +19,3 @@ Contents:
|
||||
permissions
|
||||
logging
|
||||
locking
|
||||
timemachine
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
----------------------
|
||||
|
||||
@@ -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 updateassets
|
||||
|
||||
|
||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -45,8 +45,6 @@ allow_voucher_access boolean Enables access
|
||||
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:
|
||||
@@ -121,8 +119,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
"comment": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -176,8 +173,7 @@ Endpoints
|
||||
"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
|
||||
@@ -378,10 +374,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -414,10 +407,7 @@ Endpoints
|
||||
"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
|
||||
@@ -478,10 +468,7 @@ Endpoints
|
||||
"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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
sphinx==7.3.*
|
||||
sphinx==7.2.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-e ../
|
||||
sphinx==7.3.*
|
||||
sphinx==7.2.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
@@ -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.
|
||||
=========================== ==========================================================
|
||||
@@ -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::
|
||||
|
||||
@@ -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
|
||||
@@ -449,14 +449,5 @@ 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
|
||||
|
||||
@@ -16,5 +16,4 @@ wanting to use pretix to sell tickets.
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
android-version-support
|
||||
glossary
|
||||
|
||||
@@ -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
|
||||
----------------
|
||||
|
||||
|
||||
@@ -30,36 +30,36 @@ dependencies = [
|
||||
"babel",
|
||||
"BeautifulSoup4==4.12.*",
|
||||
"bleach==5.0.*",
|
||||
"celery==5.4.*",
|
||||
"celery==5.3.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=3.4.2",
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dj-static",
|
||||
"Django[argon2]==4.2.*",
|
||||
"django-bootstrap3==24.2",
|
||||
"django-compressor==4.5",
|
||||
"Django==4.2.*",
|
||||
"django-bootstrap3==23.6.*",
|
||||
"django-compressor==4.4",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==24.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.5.*",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.4.*",
|
||||
"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-otp==1.4.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-redis==5.4.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.5.*",
|
||||
"djangorestframework==3.15.*",
|
||||
"django-statici18n==2.4.*",
|
||||
"djangorestframework==3.14.*",
|
||||
"dnspython==2.6.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.3.*",
|
||||
@@ -75,14 +75,14 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.8.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==10.4.*",
|
||||
"Pillow==10.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==5.27.*",
|
||||
"protobuf==5.26.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==4.2.*",
|
||||
"pypdf==3.9.*",
|
||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
@@ -90,9 +90,9 @@ dependencies = [
|
||||
"pyuca",
|
||||
"qrcode==7.4.*",
|
||||
"redis==5.0.*",
|
||||
"reportlab==4.2.*",
|
||||
"reportlab==4.1.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.5.*",
|
||||
"sentry-sdk==1.45.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
@@ -103,7 +103,7 @@ dependencies = [
|
||||
"ua-parser==0.18.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.2.*",
|
||||
"webauthn==2.1.*",
|
||||
"zeep==4.2.*"
|
||||
]
|
||||
|
||||
@@ -113,11 +113,11 @@ dev = [
|
||||
"aiohttp==3.9.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.23.*",
|
||||
"flake8==7.1.*",
|
||||
"fakeredis==2.21.*",
|
||||
"flake8==7.0.*",
|
||||
"freezegun",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"potypo",
|
||||
"pytest-asyncio",
|
||||
"pytest-cache",
|
||||
@@ -126,8 +126,8 @@ dev = [
|
||||
"pytest-mock==3.14.*",
|
||||
"pytest-rerunfailures==14.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.6.*",
|
||||
"pytest==8.2.*",
|
||||
"pytest-xdist==3.5.*",
|
||||
"pytest==8.1.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
|
||||
@@ -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.7.0.dev0"
|
||||
__version__ = "2024.4.0.dev0"
|
||||
|
||||
@@ -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,8 +98,6 @@ ALL_LANGUAGES = [
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('sk', _('Slovak')),
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
@@ -238,12 +235,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,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import json
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
@@ -62,57 +61,3 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class SalesChannelMigrationMixin:
|
||||
"""
|
||||
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||
"""
|
||||
|
||||
@property
|
||||
def organizer(self):
|
||||
if "organizer" in self.context:
|
||||
return self.context["organizer"]
|
||||
elif "event" in self.context:
|
||||
return self.context["event"].organizer
|
||||
else:
|
||||
raise ValueError("organizer not in context")
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if "sales_channels" in data:
|
||||
all_channels = {
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError(
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
)
|
||||
|
||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||
raise ValidationError(
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
)
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
data["limit_sales_channels"] = []
|
||||
else:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
del data["sales_channels"]
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
value = super().to_representation(value)
|
||||
if value.get("all_sales_channels"):
|
||||
value["sales_channels"] = sorted([
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||
from pretix.base.models import Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@@ -212,11 +212,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -225,10 +221,6 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
|
||||
@@ -25,20 +25,14 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -49,8 +43,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
@@ -80,6 +72,10 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
CheckinList.validate_rules(data.get('rules'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -19,27 +19,18 @@
|
||||
# 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 serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount, SalesChannel
|
||||
from pretix.base.models import Discount
|
||||
|
||||
|
||||
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||
@@ -48,7 +39,6 @@ class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -46,14 +46,10 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import (
|
||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
@@ -165,7 +161,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -174,13 +170,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
def get_event_url(self, event):
|
||||
return build_absolute_uri(event, 'presale:event.index')
|
||||
@@ -191,7 +180,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||
'sales_channels', 'best_availability_state', 'public_url')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -199,7 +188,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -281,17 +269,13 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = {
|
||||
p.module: p for p in get_all_plugins(self.instance)
|
||||
p.module for p in get_all_plugins(self.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||
|
||||
for plugin in value.get('plugins'):
|
||||
if plugin not in plugins_available:
|
||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'restricted', False):
|
||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||
|
||||
return value
|
||||
|
||||
@@ -488,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)
|
||||
@@ -700,9 +683,8 @@ 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',
|
||||
@@ -752,7 +734,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',
|
||||
|
||||
@@ -42,27 +42,19 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -71,14 +63,11 @@ class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSe
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = (
|
||||
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
|
||||
)
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
@@ -87,17 +76,10 @@ class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSe
|
||||
return value
|
||||
|
||||
|
||||
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -106,26 +88,21 @@ class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializ
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
variation = ItemVariation.objects.create(**validated_data)
|
||||
|
||||
if require_membership_types:
|
||||
variation.require_membership_types.add(*require_membership_types)
|
||||
|
||||
if limit_sales_channels:
|
||||
variation.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -246,7 +223,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -255,18 +232,11 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
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',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
@@ -289,8 +259,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
if not self.read_only:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -367,10 +335,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if limit_sales_channels:
|
||||
item.limit_sales_channels.add(*limit_sales_channels)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
if require_membership_types:
|
||||
@@ -378,13 +343,10 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
|
||||
for variation_data in variations_data:
|
||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
|
||||
var_meta_data = variation_data.pop('meta_data', {})
|
||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||
if require_membership_types:
|
||||
v.require_membership_types.add(*require_membership_types)
|
||||
if limit_sales_channels:
|
||||
v.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
if var_meta_data is not None:
|
||||
for key, value in var_meta_data.items():
|
||||
|
||||
@@ -46,12 +46,13 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -165,7 +166,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||
fields = ('id', 'name', 'seat_guid')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
@@ -563,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
|
||||
@@ -572,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)
|
||||
@@ -585,7 +583,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
@@ -713,11 +711,6 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -736,10 +729,6 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
@@ -1041,18 +1030,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
require_approval = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -1073,6 +1056,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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.')
|
||||
return channel
|
||||
|
||||
def validate_code(self, code):
|
||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||
raise ValidationError(
|
||||
@@ -1134,6 +1122,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
@@ -1142,16 +1144,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
|
||||
if not validated_data.get("sales_channel"):
|
||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||
|
||||
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||
|
||||
self._send_mail = validated_data.pop('send_email', False)
|
||||
if self._send_mail is None:
|
||||
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1311,8 +1306,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
seat_usage[seat] += 1
|
||||
sales_channel_id = validated_data['sales_channel'].identifier
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -1371,7 +1365,6 @@ 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])
|
||||
@@ -1593,9 +1586,6 @@ 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'):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
|
||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -165,36 +165,6 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
|
||||
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
type = serializers.CharField(default="api")
|
||||
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ('identifier', 'type', 'label', 'position')
|
||||
|
||||
def validate_type(self, value):
|
||||
if (not self.instance or not self.instance.pk) and value != "api":
|
||||
raise ValidationError(
|
||||
"You can currently only create channels of type 'api' through the API."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the type of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_identifier(self, value):
|
||||
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||
raise ValidationError(
|
||||
"Your identifier needs to start with 'api.'."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the identifier of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,7 +56,6 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||
|
||||
@@ -211,12 +211,8 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
|
||||
if validated_data.get('seat'):
|
||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||
if validated_data.get('sales_channel'):
|
||||
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||
else:
|
||||
sales_channel_id = "web"
|
||||
if not validated_data['seat'].is_available(
|
||||
sales_channel=sales_channel_id,
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||
):
|
||||
|
||||
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||
qs = qs.prefetch_related(
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -406,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
'item__variations').select_related('item__tax_rule')
|
||||
|
||||
if expand and 'variation' in expand:
|
||||
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||
qs = qs.prefetch_related('variation')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -60,9 +60,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
)
|
||||
return self.request.event.discounts.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
@@ -41,7 +41,6 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
@@ -58,8 +57,10 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
from pretix.presale.style import regenerate_css
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
@@ -114,10 +115,7 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(all_sales_channels=True) |
|
||||
Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
return queryset.filter(sales_channels__contains=value)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -139,12 +137,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('date_from', 'slug')
|
||||
filterset_class = EventFilter
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
**super().get_serializer_context(),
|
||||
"organizer": self.request.organizer,
|
||||
}
|
||||
|
||||
def get_copy_from_queryset(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_any_permission()
|
||||
@@ -163,20 +155,13 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = Event.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
|
||||
return qs.prefetch_related(
|
||||
'organizer',
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'item_meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
@@ -205,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()
|
||||
@@ -216,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(
|
||||
@@ -285,6 +262,8 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.is_public = serializer.validated_data['is_public']
|
||||
if 'testmode' in serializer.validated_data:
|
||||
new_event.testmode = serializer.validated_data['testmode']
|
||||
if 'sales_channels' in serializer.validated_data:
|
||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
||||
if 'has_subevents' in serializer.validated_data:
|
||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||
if 'date_admission' in serializer.validated_data:
|
||||
@@ -292,10 +271,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.save()
|
||||
if 'timezone' in serializer.validated_data:
|
||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||
|
||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
else:
|
||||
serializer.instance.set_defaults()
|
||||
|
||||
@@ -398,10 +373,7 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(event__all_sales_channels=True) |
|
||||
Q(event__limit_sales_channels__identifier=value)
|
||||
)
|
||||
return queryset.filter(event__sales_channels__contains=value)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -449,13 +421,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = SubEvent.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
|
||||
return qs.prefetch_related(
|
||||
'event',
|
||||
@@ -656,12 +622,13 @@ 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(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
|
||||
@@ -56,17 +56,10 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
@@ -78,18 +71,6 @@ with scopes_disabled():
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
class ItemVariationFilter(FilterSet):
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(value__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = ItemSerializer
|
||||
@@ -106,7 +87,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||
'variations__meta_values', 'variations__meta_values__property',
|
||||
'require_membership_types', 'variations__require_membership_types',
|
||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||
).all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -159,7 +139,6 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemVariationSerializer
|
||||
queryset = ItemVariation.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||
filterset_class = ItemVariationFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
@@ -173,8 +152,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
return self.item.variations.all().prefetch_related(
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'require_membership_types',
|
||||
'limit_sales_channels',
|
||||
'require_membership_types'
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -97,7 +97,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__)
|
||||
|
||||
@@ -108,7 +107,6 @@ with scopes_disabled():
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
@@ -116,8 +114,6 @@ with scopes_disabled():
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
|
||||
payment_provider = django_filters.CharFilter(method='provider_qs')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -141,11 +137,6 @@ with scopes_disabled():
|
||||
)
|
||||
return qs
|
||||
|
||||
def provider_qs(self, qs, name, value):
|
||||
return qs.filter(Exists(
|
||||
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
|
||||
))
|
||||
|
||||
def subevent_before_qs(self, qs, name, value):
|
||||
if getattr(self.request, 'event', None):
|
||||
subevents = self.request.event.subevents
|
||||
@@ -237,7 +228,7 @@ class OrderViewSetMixin:
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
|
||||
def _positions_prefetch(self, request):
|
||||
@@ -324,11 +315,6 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
|
||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
@@ -589,10 +575,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()
|
||||
@@ -1921,7 +1905,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:
|
||||
@@ -1929,10 +1912,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(
|
||||
|
||||
@@ -24,11 +24,10 @@ from decimal import Decimal
|
||||
import django_filters
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import transaction
|
||||
from django.db.models import OuterRef, Q, Subquery, Sum
|
||||
from django.db.models import OuterRef, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, views, viewsets
|
||||
@@ -44,16 +43,18 @@ from pretix.api.serializers.organizer import (
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||
TeamInvite, User,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
)
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.presale.style import regenerate_organizer_css
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -137,19 +138,11 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
with scopes_disabled():
|
||||
class GiftCardFilter(FilterSet):
|
||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||
expired = django_filters.BooleanFilter(method='expired_qs')
|
||||
value = django_filters.NumberFilter(field_name='cached_value')
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'testmode']
|
||||
|
||||
def expired_qs(self, qs, name, value):
|
||||
if value:
|
||||
return qs.filter(expires__isnull=False, expires__lt=now())
|
||||
else:
|
||||
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
|
||||
|
||||
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
@@ -511,6 +504,8 @@ class OrganizerSettingsView(views.APIView):
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
@@ -684,68 +679,3 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class SalesChannelFilter(FilterSet):
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ['type', 'identifier']
|
||||
|
||||
|
||||
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SalesChannelSerializer
|
||||
queryset = SalesChannel.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = SalesChannelFilter
|
||||
lookup_field = 'identifier'
|
||||
lookup_url_kwarg = 'identifier'
|
||||
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.sales_channels.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(
|
||||
organizer=self.request.organizer,
|
||||
type="api"
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.saleschannel.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(
|
||||
type=serializer.instance.type,
|
||||
identifier=serializer.instance.identifier,
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.sales_channel.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
instance.log_action(
|
||||
'pretix.saleschannel.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@@ -20,83 +20,56 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import (
|
||||
register_sales_channel_types, register_sales_channels,
|
||||
)
|
||||
from pretix.base.signals import register_sales_channels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_CHANNEL_TYPES = None
|
||||
_ALL_CHANNELS = None
|
||||
|
||||
|
||||
class SalesChannelType:
|
||||
class SalesChannel:
|
||||
def __repr__(self):
|
||||
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||
return '<SalesChannel: {}>'.format(self.identifier)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
The internal identifier of this sales channel type.
|
||||
The internal identifier of this sales channel.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name of this sales channel type.
|
||||
A human-readable name of this sales channel.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""
|
||||
A human-readable description of this sales channel type.
|
||||
"""
|
||||
return ""
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""
|
||||
This can be:
|
||||
|
||||
- The name of a Font Awesome icon to represent this channel type.
|
||||
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||
The name of a Font Awesome icon to represent this channel
|
||||
"""
|
||||
return "circle"
|
||||
|
||||
@property
|
||||
def default_created(self) -> bool:
|
||||
"""
|
||||
Indication, if a sales channel of this type should automatically be created for every organizer
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def multiple_allowed(self) -> bool:
|
||||
"""
|
||||
Indication, if multiple sales channels of this type may exist in the same organizer
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Indication, if a sales channel of this type supports test mode orders
|
||||
Indication, if a saleschannels supports test mode orders
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def payment_restrictions_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
||||
|
||||
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||
"""
|
||||
return True
|
||||
@@ -104,8 +77,8 @@ class SalesChannelType:
|
||||
@property
|
||||
def unlimited_items_per_order(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||
amount of items defined in the event settings.
|
||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
||||
items defined in the event settings.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -123,67 +96,34 @@ class SalesChannelType:
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def required_event_plugin(self) -> str:
|
||||
"""
|
||||
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
def get_all_sales_channel_types():
|
||||
from pretix.base.signals import register_sales_channel_types
|
||||
global _ALL_CHANNEL_TYPES
|
||||
|
||||
if _ALL_CHANNEL_TYPES:
|
||||
return _ALL_CHANNEL_TYPES
|
||||
if _ALL_CHANNELS:
|
||||
return _ALL_CHANNELS
|
||||
|
||||
channels = []
|
||||
for recv, ret in register_sales_channel_types.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
channels.append(ret)
|
||||
for recv, ret in register_sales_channels.send(None): # todo: remove me
|
||||
for recv, ret in register_sales_channels.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
channels.append(ret)
|
||||
channels.sort(key=lambda c: c.identifier)
|
||||
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNEL_TYPES:
|
||||
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||
return _ALL_CHANNEL_TYPES
|
||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNELS:
|
||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
||||
return _ALL_CHANNELS
|
||||
|
||||
|
||||
def get_all_sales_channels():
|
||||
# TODO: remove me
|
||||
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return get_all_sales_channel_types()
|
||||
|
||||
|
||||
class WebshopSalesChannelType(SalesChannelType):
|
||||
class WebshopSalesChannel(SalesChannel):
|
||||
identifier = "web"
|
||||
verbose_name = _('Online shop')
|
||||
icon = "globe"
|
||||
|
||||
|
||||
class ApiSalesChannelType(SalesChannelType):
|
||||
identifier = "api"
|
||||
verbose_name = _('API')
|
||||
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
|
||||
icon = "exchange"
|
||||
default_created = False
|
||||
multiple_allowed = True
|
||||
|
||||
|
||||
SalesChannel = SalesChannelType # TODO: remove me
|
||||
|
||||
|
||||
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
|
||||
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
|
||||
def base_sales_channels(sender, **kwargs):
|
||||
return (
|
||||
WebshopSalesChannelType(),
|
||||
ApiSalesChannelType(),
|
||||
WebshopSalesChannel(),
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -27,6 +27,7 @@ from openpyxl.styles import Alignment
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from ...helpers.safe_openpyxl import SafeCell
|
||||
from ..channels import get_all_sales_channels
|
||||
from ..exporter import ListExporter
|
||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||
from ..signals import register_data_exporters
|
||||
@@ -52,7 +53,7 @@ class ItemDataExporter(ListExporter):
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
locales = self.event.settings.locales
|
||||
scs = self.organizer.sales_channels.all()
|
||||
scs = get_all_sales_channels()
|
||||
header = [
|
||||
_("Product ID"),
|
||||
_("Variation ID"),
|
||||
@@ -140,15 +141,9 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append(v.value.localize(l))
|
||||
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
if not v.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active and v.active else "",
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
||||
v.default_price or i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
@@ -191,12 +186,9 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append("")
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active else "",
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
||||
i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
|
||||
@@ -54,7 +54,6 @@ class JSONExporter(BaseExporter):
|
||||
'import in third-party systems.')
|
||||
|
||||
def render(self, form_data):
|
||||
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||
jo = {
|
||||
'event': {
|
||||
'name': str(self.event.name),
|
||||
@@ -86,7 +85,7 @@ class JSONExporter(BaseExporter):
|
||||
'admission': item.admission,
|
||||
'personalized': item.personalized,
|
||||
'active': item.active,
|
||||
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
|
||||
'sales_channels': item.sales_channels,
|
||||
'description': str(item.description),
|
||||
'available_from': item.available_from,
|
||||
'available_until': item.available_until,
|
||||
@@ -115,9 +114,7 @@ class JSONExporter(BaseExporter):
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': [
|
||||
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
|
||||
],
|
||||
'sales_channels': variation.sales_channels,
|
||||
'available_from': variation.available_from,
|
||||
'available_until': variation.available_until,
|
||||
'hide_without_voucher': variation.hide_without_voucher,
|
||||
@@ -125,7 +122,6 @@ class JSONExporter(BaseExporter):
|
||||
} for variation in item.variations.all()
|
||||
]
|
||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemMetaValue.objects.select_related('property'),
|
||||
@@ -134,7 +130,6 @@ class JSONExporter(BaseExporter):
|
||||
Prefetch(
|
||||
'variations',
|
||||
queryset=ItemVariation.objects.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemVariationMetaValue.objects.select_related('property'),
|
||||
@@ -172,7 +167,7 @@ class JSONExporter(BaseExporter):
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel.identifier,
|
||||
'sales_channel': order.sales_channel,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
'fees': [
|
||||
|
||||
@@ -32,12 +32,11 @@
|
||||
# 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
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||
Q, Subquery, Sum, When,
|
||||
@@ -55,7 +54,7 @@ from openpyxl.comments import Comment
|
||||
from openpyxl.styles import Font, PatternFill
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
@@ -542,22 +541,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).order_by()
|
||||
qs = base_qs.annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
checked_in_lists=Subquery(
|
||||
Checkin.objects.filter(
|
||||
successful=True,
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
position=OuterRef("pk"),
|
||||
).order_by().values("position").annotate(
|
||||
c=GroupConcat(
|
||||
"list__name",
|
||||
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
|
||||
# anyways.
|
||||
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
delimiter=", "
|
||||
)
|
||||
).values("c")
|
||||
),
|
||||
).select_related(
|
||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||
'voucher', 'tax_rule'
|
||||
@@ -622,9 +605,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)
|
||||
@@ -634,9 +618,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'),
|
||||
@@ -655,7 +636,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Sales channel'), _('Order locale'),
|
||||
_('E-mail address verified'),
|
||||
_('External customer ID'),
|
||||
_('Check-in lists'),
|
||||
_('Payment providers'),
|
||||
]
|
||||
|
||||
@@ -747,7 +727,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
|
||||
@@ -760,10 +740,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, ''))
|
||||
|
||||
@@ -794,7 +770,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Yes') if order.email_known_to_work else _('No'),
|
||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||
]
|
||||
row.append(op.checked_in_lists or "")
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
|
||||
@@ -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 = '*****'
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -607,13 +606,13 @@ 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()
|
||||
min_date = 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():
|
||||
if pos.used_membership.date_start >= 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:
|
||||
@@ -1035,7 +1034,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 +1170,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -256,6 +256,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# pages
|
||||
return resp
|
||||
|
||||
resp['X-XSS-Protection'] = '1'
|
||||
|
||||
# We just need to have a P3P, not matter whats in there
|
||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
|
||||
|
||||
def set_sales_channels(apps, schema_editor):
|
||||
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
|
||||
# Therefore, for existing events, we enable all sales channels
|
||||
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
|
||||
batch_size = 1000
|
||||
Event_SettingsStore.objects.bulk_create([
|
||||
Event_SettingsStore(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.db import migrations
|
||||
|
||||
import pretix.base.models.fields
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='sales_channels',
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,110 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-03-24 17:43
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0264_order_internal_secret"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SalesChannel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("label", i18nfield.fields.I18nCharField(max_length=200)),
|
||||
("identifier", models.CharField(max_length=200)),
|
||||
("type", models.CharField(max_length=200)),
|
||||
("position", models.PositiveIntegerField(default=0)),
|
||||
("configuration", models.JSONField(default=dict)),
|
||||
],
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="checkinlist",
|
||||
old_name="auto_checkin_sales_channels",
|
||||
new_name="auto_checkin_sales_channel_types",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="discount",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="order",
|
||||
old_name="sales_channel",
|
||||
new_name="sales_channel_type",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="saleschannel",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sales_channels",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="discount",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="checkinlist",
|
||||
name="auto_checkin_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="sales_channel",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.saleschannel",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="saleschannel",
|
||||
unique_together={("organizer", "identifier")},
|
||||
),
|
||||
]
|
||||
@@ -1,84 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-03-24 17:55
|
||||
|
||||
from django.db import migrations
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
def create_sales_channels(apps, schema_editor):
|
||||
channel_types = get_all_sales_channel_types()
|
||||
type_to_channel = dict()
|
||||
full_discount_set = set()
|
||||
full_set = set()
|
||||
|
||||
Organizer = apps.get_model("pretixbase", "Organizer")
|
||||
for o in Organizer.objects.all().iterator():
|
||||
for i, t in enumerate(channel_types.values()):
|
||||
if not t.default_created:
|
||||
continue
|
||||
type_to_channel[t.identifier, o.pk] = o.sales_channels.get_or_create(
|
||||
type=t.identifier,
|
||||
defaults=dict(
|
||||
position=i,
|
||||
identifier=t.identifier,
|
||||
label=LazyI18nString.from_gettext(t.verbose_name),
|
||||
),
|
||||
)[0]
|
||||
full_set.add(t.identifier)
|
||||
if t.discounts_supported:
|
||||
full_discount_set.add(t.identifier)
|
||||
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
for d in Event.objects.all().iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.organizer_id])
|
||||
|
||||
Item = apps.get_model("pretixbase", "Item")
|
||||
for d in Item.objects.select_related("event").iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||
|
||||
ItemVariation = apps.get_model("pretixbase", "ItemVariation")
|
||||
for d in ItemVariation.objects.select_related("item__event").iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.item.event.organizer_id])
|
||||
|
||||
Discount = apps.get_model("pretixbase", "Discount")
|
||||
for d in Discount.objects.select_related("event").iterator():
|
||||
if set(d.sales_channels) != full_discount_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||
|
||||
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||
for c in CheckinList.objects.select_related("event").iterator():
|
||||
for s in c.auto_checkin_sales_channel_types:
|
||||
c.auto_checkin_sales_channels.add(type_to_channel[s, c.event.organizer_id])
|
||||
|
||||
Order = apps.get_model("pretixbase", "Order")
|
||||
for (k, orgid), v in type_to_channel.items():
|
||||
Order.objects.filter(sales_channel_type=k, event__organizer_id=orgid, sales_channel__isnull=True).update(
|
||||
sales_channel=v
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0265_saleschannel_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_sales_channels, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-03-25 13:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0266_saleschannel_migrate_data"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="checkinlist",
|
||||
name="auto_checkin_sales_channel_types",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="discount",
|
||||
name="sales_channels",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="event",
|
||||
name="sales_channels",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="item",
|
||||
name="sales_channels",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="itemvariation",
|
||||
name="sales_channels",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="order",
|
||||
name="sales_channel_type",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="order",
|
||||
name="sales_channel",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.saleschannel",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-07-01 09:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0267_remove_old_sales_channels"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="subevent",
|
||||
name="items",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="subevent",
|
||||
name="variations",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="order",
|
||||
name="internal_secret",
|
||||
field=models.CharField(
|
||||
default=pretix.base.models.orders.generate_secret,
|
||||
max_length=32,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -19,7 +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 datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import pycountry
|
||||
@@ -27,7 +26,6 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import Q
|
||||
from django.utils import formats
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import (
|
||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||
@@ -38,6 +36,7 @@ from i18nfield.strings import LazyI18nString
|
||||
from phonenumber_field.phonenumber import to_python
|
||||
from phonenumbers import SUPPORTED_REGIONS
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.modelimport import (
|
||||
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
|
||||
@@ -510,55 +509,21 @@ class ValidUntil(DatetimeColumnMixin, ImportColumn):
|
||||
position.valid_until = value
|
||||
|
||||
|
||||
class Expires(DatetimeColumnMixin, ImportColumn):
|
||||
identifier = 'expires'
|
||||
verbose_name = gettext_lazy('Expiry date')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
return
|
||||
|
||||
input_formats = formats.get_format('DATE_INPUT_FORMATS', use_l10n=True)
|
||||
for format in input_formats:
|
||||
try:
|
||||
d = datetime.datetime.strptime(value, format)
|
||||
d = d.replace(tzinfo=self.timezone, hour=23, minute=59, second=59)
|
||||
return d
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return super().clean(value, previous_values) # parse date
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value:
|
||||
order.expires = value
|
||||
|
||||
|
||||
class Saleschannel(ImportColumn):
|
||||
identifier = 'sales_channel'
|
||||
verbose_name = gettext_lazy('Sales channel')
|
||||
default_value = None
|
||||
initial = 'static:web'
|
||||
|
||||
@cached_property
|
||||
def channels(self):
|
||||
return list(self.event.organizer.sales_channels.all())
|
||||
|
||||
def static_choices(self):
|
||||
return [
|
||||
(c.identifier, str(c.label)) for c in self.channels
|
||||
(sc.identifier, sc.verbose_name) for sc in get_all_sales_channels().values()
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
matches = [
|
||||
p for p in self.channels
|
||||
if p.identifier == value or any((v and v == value) for v in i18n_flat(p.label))
|
||||
]
|
||||
if len(matches) == 0:
|
||||
if not value:
|
||||
value = 'web'
|
||||
if value not in get_all_sales_channels():
|
||||
raise ValidationError(_("Please enter a valid sales channel."))
|
||||
if len(matches) > 1:
|
||||
raise ValidationError(_("Please enter a valid sales channel."))
|
||||
return matches[0]
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.sales_channel = value
|
||||
@@ -737,13 +702,12 @@ def get_order_import_columns(event):
|
||||
AttendeeState(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
SeatColumn(event),
|
||||
ValidFrom(event),
|
||||
ValidUntil(event),
|
||||
Locale(event),
|
||||
Saleschannel(event),
|
||||
Expires(event),
|
||||
SeatColumn(event),
|
||||
Comment(event),
|
||||
ValidFrom(event),
|
||||
ValidUntil(event),
|
||||
]
|
||||
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
|
||||
default.append(QuestionColumn(event, q))
|
||||
|
||||
@@ -66,7 +66,6 @@ class SubeventColumn(ImportColumn):
|
||||
class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
|
||||
identifier = 'max_usages'
|
||||
verbose_name = gettext_lazy('Maximum usages')
|
||||
default_value = None
|
||||
initial = "static:1"
|
||||
|
||||
def static_choices(self):
|
||||
@@ -74,11 +73,6 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
|
||||
("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
|
||||
|
||||
@@ -86,7 +80,6 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
|
||||
class MinUsagesColumn(IntegerColumnMixin, ImportColumn):
|
||||
identifier = 'min_usages'
|
||||
verbose_name = gettext_lazy('Minimum usages')
|
||||
default_value = None
|
||||
initial = "static:1"
|
||||
|
||||
def static_choices(self):
|
||||
@@ -253,7 +246,7 @@ class QuotaColumn(ImportColumn):
|
||||
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 str(q.pk) == value or any((v and v == value) for v in i18n_flat(q.name))
|
||||
]
|
||||
if len(matches) == 0:
|
||||
raise ValidationError(_("No matching variation was found."))
|
||||
|
||||
@@ -51,8 +51,7 @@ from .orders import (
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, SalesChannel, Team, TeamAPIToken,
|
||||
TeamInvite,
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
)
|
||||
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
||||
from .tax import TaxRule
|
||||
|
||||
@@ -418,22 +418,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:
|
||||
|
||||
@@ -46,6 +46,7 @@ from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.helpers import PostgresWindowFrame
|
||||
|
||||
|
||||
@@ -99,13 +100,13 @@ class CheckinList(LoggedModel):
|
||||
verbose_name=_('Automatically check out everyone at'),
|
||||
null=True, blank=True
|
||||
)
|
||||
auto_checkin_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
auto_checkin_sales_channels = MultiStringField(
|
||||
default=[],
|
||||
blank=True,
|
||||
verbose_name=_('Sales channels to automatically check in'),
|
||||
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
|
||||
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.'),
|
||||
blank=True,
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.')
|
||||
)
|
||||
rules = models.JSONField(default=dict, blank=True)
|
||||
|
||||
@@ -284,7 +285,7 @@ class CheckinList(LoggedModel):
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -34,6 +33,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
|
||||
@@ -64,14 +64,10 @@ class Discount(LoggedModel):
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("All supported sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Sales channels"),
|
||||
blank=True,
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=['web'],
|
||||
blank=False,
|
||||
)
|
||||
|
||||
available_from = models.DateTimeField(
|
||||
@@ -276,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:
|
||||
|
||||
@@ -36,7 +36,6 @@ import logging
|
||||
import os
|
||||
import string
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
@@ -46,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 (
|
||||
@@ -67,8 +65,8 @@ from django_scopes import ScopedManager, scopes_disabled
|
||||
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
|
||||
@@ -236,7 +234,7 @@ class EventMixin:
|
||||
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 self.settings.waiting_list_auto_disable.datetime(self) > now()
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -245,11 +243,11 @@ class EventMixin:
|
||||
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,7 +267,7 @@ 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
|
||||
|
||||
@@ -304,12 +302,8 @@ class EventMixin:
|
||||
return safe_string(json.dumps(eventdict))
|
||||
|
||||
@classmethod
|
||||
def annotated(cls, qs, channel, voucher=None):
|
||||
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||
# makes the query SIGNIFICANTLY faster
|
||||
from pretix.base.models import Item, ItemVariation, Quota, SalesChannel
|
||||
|
||||
assert isinstance(channel, (SalesChannel, str))
|
||||
def annotated(cls, qs, channel='web', voucher=None):
|
||||
from pretix.base.models import Item, ItemVariation, Quota
|
||||
|
||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||
Q(variations__isnull=True)
|
||||
@@ -320,23 +314,18 @@ class EventMixin:
|
||||
|
||||
q_variation = (
|
||||
Q(active=True)
|
||||
& 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(sales_channels__contains=channel)
|
||||
& 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)
|
||||
& Q(quotas__pk=OuterRef('pk'))
|
||||
)
|
||||
|
||||
if isinstance(channel, str):
|
||||
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
|
||||
else:
|
||||
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
||||
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
|
||||
|
||||
if voucher:
|
||||
if voucher.variation_id:
|
||||
q_variation &= Q(pk=voucher.variation_id)
|
||||
@@ -476,7 +465,6 @@ class EventMixin:
|
||||
return best_state_found, num_tickets_found, num_tickets_possible
|
||||
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
assert isinstance(sales_channel, str) or sales_channel is None
|
||||
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
|
||||
|
||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
@@ -505,13 +493,10 @@ class EventMixin:
|
||||
return qs.filter(q)
|
||||
|
||||
|
||||
def default_sales_channels(): # kept for legacy migration
|
||||
from ..channels import get_all_sales_channel_types
|
||||
def default_sales_channels():
|
||||
from ..channels import get_all_sales_channels
|
||||
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||
|
||||
return list(get_all_sales_channel_types().keys())
|
||||
return list(get_all_sales_channels().keys())
|
||||
|
||||
|
||||
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
||||
@@ -548,10 +533,8 @@ class Event(EventMixin, LoggedModel):
|
||||
:type plugins: str
|
||||
:param has_subevents: Enable event series functionality
|
||||
:type has_subevents: bool
|
||||
:param all_sales_channels: A flag indicating that this event is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
||||
:type limit_sales_channels: list
|
||||
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
||||
:type sales_channels: list
|
||||
"""
|
||||
|
||||
settings_namespace = 'event'
|
||||
@@ -643,14 +626,10 @@ class Event(EventMixin, LoggedModel):
|
||||
auto_now=True, db_index=True
|
||||
)
|
||||
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
blank=True,
|
||||
sales_channels = MultiStringField(
|
||||
verbose_name=_('Restrict to specific sales channels'),
|
||||
help_text=_('Only sell tickets for this event on the following sales channels.'),
|
||||
default=default_sales_channels,
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
@@ -715,7 +694,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
|
||||
|
||||
@@ -808,6 +787,8 @@ class Event(EventMixin, LoggedModel):
|
||||
), tz)
|
||||
|
||||
def copy_data_from(self, other, skip_meta_data=False):
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
from ..signals import event_copy_data
|
||||
from . import (
|
||||
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
|
||||
@@ -824,17 +805,10 @@ class Event(EventMixin, LoggedModel):
|
||||
if other.date_admission:
|
||||
self.date_admission = self.date_from + (other.date_admission - other.date_from)
|
||||
self.testmode = other.testmode
|
||||
self.all_sales_channels = other.all_sales_channels
|
||||
self.sales_channels = other.sales_channels
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
if not self.all_sales_channels:
|
||||
self.limit_sales_channels.set(
|
||||
self.organizer.sales_channels.filter(
|
||||
identifier__in=other.limit_sales_channels.values_list("identifier", flat=True)
|
||||
)
|
||||
)
|
||||
|
||||
if not skip_meta_data:
|
||||
for emv in EventMetaValue.objects.filter(event=other):
|
||||
emv.pk = None
|
||||
@@ -872,17 +846,12 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
item_map = {}
|
||||
variation_map = {}
|
||||
for i in Item.objects.filter(event=other).prefetch_related(
|
||||
'variations', 'limit_sales_channels', 'require_membership_types',
|
||||
'variations__limit_sales_channels', 'variations__require_membership_types',
|
||||
):
|
||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
||||
vars = list(i.variations.all())
|
||||
require_membership_types = list(i.require_membership_types.all())
|
||||
limit_sales_channels = list(i.limit_sales_channels.all())
|
||||
item_map[i.pk] = i
|
||||
i.pk = None
|
||||
i.event = self
|
||||
i._prefetched_objects_cache = {}
|
||||
if i.picture:
|
||||
i.picture.save(os.path.basename(i.picture.name), i.picture)
|
||||
if i.category_id:
|
||||
@@ -899,23 +868,12 @@ class Event(EventMixin, LoggedModel):
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
i.require_membership_types.set(require_membership_types)
|
||||
|
||||
if not i.all_sales_channels:
|
||||
i.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
for v in vars:
|
||||
require_membership_types = list(v.require_membership_types.all())
|
||||
limit_sales_channels = list(v.limit_sales_channels.all())
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
v.item = i
|
||||
v._prefetched_objects_cache = {}
|
||||
v.save(force_insert=True)
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
v.require_membership_types.set(require_membership_types)
|
||||
if not v.all_sales_channels:
|
||||
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
||||
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
||||
i.save()
|
||||
@@ -953,7 +911,6 @@ class Event(EventMixin, LoggedModel):
|
||||
vars = list(q.variations.all())
|
||||
oldid = q.pk
|
||||
q.pk = None
|
||||
q._prefetched_objects_cache = {}
|
||||
q.event = self
|
||||
q.closed = False
|
||||
q.save(force_insert=True)
|
||||
@@ -965,15 +922,11 @@ class Event(EventMixin, LoggedModel):
|
||||
q.variations.add(variation_map[v.pk])
|
||||
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
|
||||
|
||||
for d in Discount.objects.filter(event=other).prefetch_related(
|
||||
'condition_limit_products', 'benefit_limit_products', 'limit_sales_channels'
|
||||
):
|
||||
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
|
||||
c_items = list(d.condition_limit_products.all())
|
||||
b_items = list(d.benefit_limit_products.all())
|
||||
limit_sales_channels = list(d.limit_sales_channels.all())
|
||||
d.pk = None
|
||||
d.event = self
|
||||
d._prefetched_objects_cache = {}
|
||||
d.save(force_insert=True)
|
||||
d.log_action('pretix.object.cloned')
|
||||
for i in c_items:
|
||||
@@ -983,16 +936,12 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.pk in item_map:
|
||||
d.benefit_limit_products.add(item_map[i.pk])
|
||||
|
||||
if not d.all_sales_channels:
|
||||
d.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
question_map = {}
|
||||
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
|
||||
items = list(q.items.all())
|
||||
opts = list(q.options.all())
|
||||
question_map[q.pk] = q
|
||||
q.pk = None
|
||||
q._prefetched_objects_cache = {}
|
||||
q.event = self
|
||||
q.save(force_insert=True)
|
||||
q.log_action('pretix.object.cloned')
|
||||
@@ -1023,14 +972,10 @@ class Event(EventMixin, LoggedModel):
|
||||
_walk_rules(i)
|
||||
|
||||
checkin_list_map = {}
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||
'limit_products', 'auto_checkin_sales_channels'
|
||||
):
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||
items = list(cl.limit_products.all())
|
||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
||||
checkin_list_map[cl.pk] = cl
|
||||
cl.pk = None
|
||||
cl._prefetched_objects_cache = {}
|
||||
cl.event = self
|
||||
rules = cl.rules
|
||||
_walk_rules(rules)
|
||||
@@ -1039,8 +984,6 @@ class Event(EventMixin, LoggedModel):
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
if auto_checkin_sales_channels:
|
||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
||||
|
||||
if other.seating_plan:
|
||||
if other.seating_plan.organizer_id == self.organizer_id:
|
||||
@@ -1066,10 +1009,10 @@ class Event(EventMixin, LoggedModel):
|
||||
s.product = item_map[s.product_id]
|
||||
s.save(force_insert=True)
|
||||
|
||||
has_custom_style = other.settings.presale_css_file or other.settings.presale_widget_css_file
|
||||
skip_settings = (
|
||||
'ticket_secrets_pretix_sig1_pubkey',
|
||||
'ticket_secrets_pretix_sig1_privkey',
|
||||
# no longer used, but we still don't need to copy them
|
||||
'presale_css_file',
|
||||
'presale_css_checksum',
|
||||
'presale_widget_css_file',
|
||||
@@ -1082,7 +1025,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))
|
||||
@@ -1112,6 +1055,9 @@ class Event(EventMixin, LoggedModel):
|
||||
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
|
||||
)
|
||||
|
||||
if has_custom_style:
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
def get_payment_providers(self, cached=False) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized payment providers mapped by their identifiers.
|
||||
@@ -1241,8 +1187,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"):
|
||||
@@ -1291,9 +1237,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.'))
|
||||
|
||||
@@ -1389,12 +1332,18 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||
plugins_active = self.get_plugins()
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
def disable_plugin(self, module):
|
||||
plugins_active = self.get_plugins()
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
self.set_active_plugins(plugins_active)
|
||||
@@ -1403,6 +1352,8 @@ class Event(EventMixin, LoggedModel):
|
||||
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
||||
getattr(plugins_available[module].app, 'uninstalled')(self)
|
||||
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
@staticmethod
|
||||
def clean_has_subevents(event, has_subevents):
|
||||
if event is not None and event.has_subevents is not None:
|
||||
@@ -1507,15 +1458,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
|
||||
@@ -1544,18 +1493,15 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
return qs_annotated
|
||||
|
||||
@classmethod
|
||||
def annotated(cls, qs, channel, voucher=None):
|
||||
def annotated(cls, qs, channel='web', voucher=None):
|
||||
from .items import SubEventItem, SubEventItemVariation
|
||||
from .organizer import SalesChannel
|
||||
|
||||
assert isinstance(channel, (str, SalesChannel))
|
||||
|
||||
qs = super().annotated(qs, channel, voucher=voucher)
|
||||
qs = qs.annotate(
|
||||
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(),
|
||||
@@ -1566,7 +1512,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(),
|
||||
|
||||
@@ -34,10 +34,8 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import calendar
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
@@ -57,16 +55,16 @@ 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
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
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
|
||||
@@ -123,16 +121,6 @@ class ItemCategory(LoggedModel):
|
||||
return _('{category} (Add-On products)').format(category=str(name))
|
||||
return str(name)
|
||||
|
||||
def get_category_type_display(self):
|
||||
if self.is_addon:
|
||||
return _('Add-On products')
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def category_type(self):
|
||||
return 'addon' if self.is_addon else 'normal'
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
@@ -204,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:
|
||||
@@ -260,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:
|
||||
@@ -271,24 +259,14 @@ class SubEventItemVariation(models.Model):
|
||||
|
||||
|
||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||
# makes the query SIGNIFICANTLY faster
|
||||
from .organizer import SalesChannel
|
||||
|
||||
assert isinstance(channel, (SalesChannel, str))
|
||||
q = (
|
||||
# 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(require_bundling=False)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if isinstance(channel, str):
|
||||
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||
else:
|
||||
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
||||
|
||||
if not allow_addons:
|
||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||
|
||||
@@ -364,10 +342,8 @@ class Item(LoggedModel):
|
||||
:type original_price: decimal.Decimal
|
||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||
:type require_approval: bool
|
||||
:param all_sales_channels: A flag indicating that this item is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this item is available for sale on.
|
||||
:type limit_sales_channels: list
|
||||
:param sales_channels: Sales channels this item is available on.
|
||||
:type sales_channels: bool
|
||||
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
|
||||
:type issue_giftcard: bool
|
||||
:param validity_mode: Instruction how to set ``valid_from``/``valid_until`` on tickets, ``null`` is default event validity.
|
||||
@@ -467,8 +443,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(
|
||||
@@ -622,14 +597,9 @@ class Item(LoggedModel):
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('Only sell tickets for this product on the selected sales channels.'),
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=['web'],
|
||||
blank=True,
|
||||
)
|
||||
issue_giftcard = models.BooleanField(
|
||||
@@ -812,7 +782,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:
|
||||
@@ -824,13 +794,13 @@ 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()
|
||||
now_dt = now_dt or now()
|
||||
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
@@ -987,11 +957,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)
|
||||
|
||||
@@ -1051,13 +1021,9 @@ class Item(LoggedModel):
|
||||
return None, None
|
||||
|
||||
|
||||
def _all_sales_channels_identifiers(): # kept for legacy migrations
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||
|
||||
return list(get_all_sales_channel_types().keys())
|
||||
def _all_sales_channels_identifiers():
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
return list(get_all_sales_channels().keys())
|
||||
|
||||
|
||||
class ItemVariation(models.Model):
|
||||
@@ -1080,10 +1046,6 @@ class ItemVariation(models.Model):
|
||||
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
||||
approval by an administrator
|
||||
:type require_approval: bool
|
||||
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||
:type limit_sales_channels: list
|
||||
|
||||
"""
|
||||
item = models.ForeignKey(
|
||||
@@ -1123,8 +1085,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(
|
||||
@@ -1169,13 +1130,9 @@ class ItemVariation(models.Model):
|
||||
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=_all_sales_channels_identifiers,
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
blank=True,
|
||||
@@ -1333,7 +1290,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:
|
||||
@@ -1345,13 +1302,13 @@ 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()
|
||||
now_dt = now_dt or now()
|
||||
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
|
||||
@@ -23,6 +23,7 @@ from django.db import models
|
||||
from django.db.models import Count, OuterRef, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField
|
||||
@@ -30,7 +31,6 @@ from i18nfield.fields import I18nCharField
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers.names import build_name
|
||||
|
||||
|
||||
@@ -165,13 +165,13 @@ class Membership(models.Model):
|
||||
|
||||
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
||||
if valid_from_not_chosen:
|
||||
return not self.canceled and self.date_end >= time_machine_now()
|
||||
return not self.canceled and self.date_end >= now()
|
||||
elif ticket_valid_from:
|
||||
dt = ticket_valid_from
|
||||
elif ev:
|
||||
dt = ev.date_from
|
||||
else:
|
||||
dt = time_machine_now()
|
||||
dt = now()
|
||||
|
||||
return not self.canceled and dt >= self.date_start and dt <= self.date_end
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
@@ -60,7 +59,7 @@ from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
@@ -81,7 +80,6 @@ from pretix.base.models import Customer, User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
@@ -106,34 +104,6 @@ def generate_position_secret():
|
||||
raise TypeError("Function no longer exists, use secret generators")
|
||||
|
||||
|
||||
class OrderQuerySet(models.QuerySet):
|
||||
def get_with_secret_check(self, code, received_secret, tag, secret_length=64):
|
||||
dummy = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"[:secret_length]
|
||||
try:
|
||||
order = self.get(code=code)
|
||||
except Order.DoesNotExist:
|
||||
# Do a hash comparison as well to harden against timing attacks
|
||||
hmac.compare_digest(
|
||||
salted_hmac(key_salt=b"", value=tag, algorithm="sha256",
|
||||
secret=dummy).hexdigest()[:secret_length],
|
||||
received_secret[:secret_length]
|
||||
)
|
||||
raise Order.DoesNotExist
|
||||
|
||||
if not hmac.compare_digest(
|
||||
order.tagged_secret(tag, secret_length) if tag else order.secret,
|
||||
received_secret[:secret_length].lower() if tag else received_secret.lower()
|
||||
) and not (
|
||||
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
||||
tag and hmac.compare_digest(
|
||||
hashlib.sha1(order.secret.lower().encode()).hexdigest(),
|
||||
received_secret.lower()
|
||||
)
|
||||
):
|
||||
raise Order.DoesNotExist
|
||||
return order
|
||||
|
||||
|
||||
class Order(LockModel, LoggedModel):
|
||||
"""
|
||||
An order is created when a user clicks 'buy' on his cart. It holds
|
||||
@@ -187,8 +157,8 @@ class Order(LockModel, LoggedModel):
|
||||
:type require_approval: bool
|
||||
:param meta_info: Additional meta information on the order, JSON-encoded.
|
||||
:type meta_info: str
|
||||
:param sales_channel: Foreign key to the sales channel this order was created through.
|
||||
:type sales_channel: SalesChannel
|
||||
:param sales_channel: Identifier of the sales channel this order was created through.
|
||||
:type sales_channel: str
|
||||
"""
|
||||
|
||||
STATUS_PENDING = "n"
|
||||
@@ -252,7 +222,6 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name=_('Locale')
|
||||
)
|
||||
secret = models.CharField(max_length=32, default=generate_secret)
|
||||
internal_secret = models.CharField(null=True, blank=True, max_length=32, default=generate_secret)
|
||||
datetime = models.DateTimeField(
|
||||
verbose_name=_("Date"), db_index=False
|
||||
)
|
||||
@@ -305,10 +274,7 @@ class Order(LockModel, LoggedModel):
|
||||
require_approval = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
sales_channel = models.ForeignKey(
|
||||
"SalesChannel",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
sales_channel = models.CharField(max_length=190, default="web")
|
||||
email_known_to_work = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('E-mail address verified')
|
||||
@@ -318,7 +284,7 @@ class Order(LockModel, LoggedModel):
|
||||
default=False,
|
||||
)
|
||||
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -715,7 +681,7 @@ class Order(LockModel, LoggedModel):
|
||||
for op in positions:
|
||||
if op.issued_gift_cards.all():
|
||||
return False
|
||||
if self.user_change_deadline and time_machine_now() > self.user_change_deadline:
|
||||
if self.user_change_deadline and now() > self.user_change_deadline:
|
||||
return False
|
||||
|
||||
return (
|
||||
@@ -747,7 +713,7 @@ class Order(LockModel, LoggedModel):
|
||||
return False
|
||||
if op.granted_memberships.with_usages().filter(usages__gt=0):
|
||||
return False
|
||||
if self.user_cancel_deadline and time_machine_now() > self.user_cancel_deadline:
|
||||
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
||||
return False
|
||||
|
||||
if self.status == Order.STATUS_PAID:
|
||||
@@ -884,11 +850,8 @@ class Order(LockModel, LoggedModel):
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
if self.event.settings.allow_modifications not in ("order", "attendee"):
|
||||
return False
|
||||
|
||||
modify_deadline = self.modify_deadline
|
||||
if modify_deadline is not None and time_machine_now() > modify_deadline:
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
@@ -940,7 +903,7 @@ class Order(LockModel, LoggedModel):
|
||||
return self.event.settings.ticket_download and (
|
||||
self.event.settings.ticket_download_date is None
|
||||
or self.ticket_download_date is None
|
||||
or time_machine_now() > self.ticket_download_date
|
||||
or now() > self.ticket_download_date
|
||||
) and (
|
||||
self.status == Order.STATUS_PAID
|
||||
or (
|
||||
@@ -1012,7 +975,7 @@ class Order(LockModel, LoggedModel):
|
||||
return error_messages['require_approval']
|
||||
term_last = self.payment_term_last
|
||||
if term_last and not ignore_date:
|
||||
if time_machine_now() > term_last:
|
||||
if now() > term_last:
|
||||
return error_messages['late_lastdate']
|
||||
|
||||
if self.status == self.STATUS_PENDING:
|
||||
@@ -1035,7 +998,7 @@ class Order(LockModel, LoggedModel):
|
||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||
}
|
||||
now_dt = now_dt or time_machine_now()
|
||||
now_dt = now_dt or now()
|
||||
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
|
||||
quota_cache = {}
|
||||
v_budget = {}
|
||||
@@ -1256,10 +1219,6 @@ class Order(LockModel, LoggedModel):
|
||||
_transactions_mark_order_clean(self.pk)
|
||||
return create
|
||||
|
||||
def tagged_secret(self, tag, secret_length=64):
|
||||
return salted_hmac(value=tag, key_salt=b"", algorithm="sha256",
|
||||
secret=self.internal_secret or self.secret).hexdigest()[:secret_length]
|
||||
|
||||
|
||||
def answerfile_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||
@@ -1935,7 +1894,7 @@ class OrderPayment(models.Model):
|
||||
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
||||
)
|
||||
|
||||
if send_mail and self.order.sales_channel.identifier in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||
self._send_paid_mail(invoice, user, mail_text)
|
||||
if self.order.event.settings.mail_send_order_paid_attendee:
|
||||
for p in self.order.positions.all():
|
||||
@@ -2554,43 +2513,6 @@ class OrderPosition(AbstractPosition):
|
||||
reasons[b] = b
|
||||
return reasons
|
||||
|
||||
@property
|
||||
def can_modify_answers(self) -> bool:
|
||||
"""
|
||||
``True`` if the user can change the question answers / attendee names that are
|
||||
related to the position. This checks order status and modification deadlines. It also
|
||||
returns ``False`` if there are no questions that can be answered.
|
||||
"""
|
||||
from .checkin import Checkin
|
||||
|
||||
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
if self.event.settings.allow_modifications != "attendee":
|
||||
return False
|
||||
|
||||
modify_deadline = self.order.modify_deadline
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
self.order.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
for cp in positions:
|
||||
if cp.has_checkin:
|
||||
return False
|
||||
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
for cp in positions:
|
||||
if cp.pk == self.pk or cp.addon_to_id == self.pk:
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
return False # nothing there to modify
|
||||
|
||||
@classmethod
|
||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||
from . import Voucher
|
||||
@@ -2613,9 +2535,9 @@ class OrderPosition(AbstractPosition):
|
||||
if cartpos.item.validity_mode:
|
||||
valid_from, valid_until = cartpos.item.compute_validity(
|
||||
requested_start=(
|
||||
max(cartpos.requested_valid_from, time_machine_now())
|
||||
max(cartpos.requested_valid_from, now())
|
||||
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
|
||||
else time_machine_now()
|
||||
else now()
|
||||
),
|
||||
enforce_start_limit=True,
|
||||
override_tz=order.event.timezone,
|
||||
@@ -3141,9 +3063,9 @@ class CartPosition(AbstractPosition):
|
||||
def predicted_validity(self):
|
||||
return self.item.compute_validity(
|
||||
requested_start=(
|
||||
max(self.requested_valid_from, time_machine_now())
|
||||
max(self.requested_valid_from, now())
|
||||
if self.requested_valid_from and self.item.validity_dynamic_start_choice
|
||||
else time_machine_now()
|
||||
else now()
|
||||
),
|
||||
override_tz=self.event.timezone,
|
||||
)
|
||||
|
||||
@@ -46,9 +46,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scope
|
||||
from i18nfield.fields import I18nCharField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||
@@ -106,8 +104,6 @@ class Organizer(LoggedModel):
|
||||
if is_new:
|
||||
kwargs.pop('update_fields', None) # does not make sense here
|
||||
self.set_defaults()
|
||||
with scope(organizer=self):
|
||||
self.create_default_sales_channels()
|
||||
else:
|
||||
self.get_cache().clear()
|
||||
return obj
|
||||
@@ -216,24 +212,6 @@ class Organizer(LoggedModel):
|
||||
else:
|
||||
return get_connection(fail_silently=False)
|
||||
|
||||
def create_default_sales_channels(self):
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
i = 0
|
||||
for channel in get_all_sales_channel_types().values():
|
||||
if not channel.default_created:
|
||||
continue
|
||||
|
||||
self.sales_channels.get_or_create(
|
||||
identifier=channel.identifier,
|
||||
defaults={
|
||||
'label': LazyI18nString.from_gettext(channel.verbose_name),
|
||||
'type': channel.identifier,
|
||||
},
|
||||
position=i
|
||||
)
|
||||
i += 1
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
@@ -526,58 +504,3 @@ class OrganizerFooterLink(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.organizer.cache.clear()
|
||||
|
||||
|
||||
class SalesChannel(LoggedModel):
|
||||
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='sales_channels')
|
||||
label = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
identifier = models.CharField(
|
||||
verbose_name=_("Identifier"),
|
||||
max_length=200,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^[a-zA-Z0-9.\-_]+$",
|
||||
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
|
||||
),
|
||||
],
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_("Type"),
|
||||
max_length=200,
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
configuration = models.JSONField(default=dict)
|
||||
|
||||
objects = ScopedManager(organizer="organizer")
|
||||
|
||||
class Meta:
|
||||
ordering = ("position", "type", "identifier", "id")
|
||||
unique_together = ("organizer", "identifier")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.label)
|
||||
|
||||
@cached_property
|
||||
def type_instance(self):
|
||||
from ..channels import get_all_sales_channel_types
|
||||
|
||||
types = get_all_sales_channel_types()
|
||||
return types[self.type]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self.type_instance.icon
|
||||
|
||||
def allow_delete(self):
|
||||
from . import Order
|
||||
|
||||
if self.type_instance.default_created:
|
||||
return False
|
||||
|
||||
return not Order.objects.filter(sales_channel=self).exists()
|
||||
|
||||
@@ -243,14 +243,10 @@ class Seat(models.Model):
|
||||
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
|
||||
return qs_annotated
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
|
||||
sales_channel='web',
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
|
||||
ignore_distancing=False, distance_ignore_cart_id=None):
|
||||
from .orders import Order
|
||||
from .organizer import SalesChannel
|
||||
|
||||
if isinstance(sales_channel, SalesChannel):
|
||||
sales_channel = sales_channel.identifier
|
||||
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(
|
||||
|
||||
@@ -370,11 +370,10 @@ class Voucher(LoggedModel):
|
||||
'redeemed': redeemed
|
||||
}
|
||||
)
|
||||
if data.get('min_usages') is not None:
|
||||
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
||||
raise ValidationError(
|
||||
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
||||
)
|
||||
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
||||
raise ValidationError(
|
||||
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_subevent(data, event):
|
||||
@@ -460,7 +459,7 @@ class Voucher(LoggedModel):
|
||||
new_quotas = set(
|
||||
Quota.objects.filter(
|
||||
pk__in=Quota.variations.through.objects.filter(
|
||||
itemvariation__item=item,
|
||||
itemvariation__item=old_instance.item,
|
||||
quota__subevent=data.get('subevent'),
|
||||
).values('quota_id')
|
||||
)
|
||||
|
||||
@@ -56,7 +56,8 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
||||
OrderRefund, Quota, TaxRule,
|
||||
@@ -66,7 +67,6 @@ from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.format import format_map
|
||||
@@ -416,8 +416,8 @@ class BasePaymentProvider:
|
||||
forms.MultipleChoiceField(
|
||||
label=_('Restrict to specific sales channels'),
|
||||
choices=(
|
||||
(c.identifier, c.label) for c in self.event.organizer.sales_channels.all()
|
||||
if c.type_instance.payment_restrictions_supported
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
if c.payment_restrictions_supported
|
||||
),
|
||||
initial=['web'],
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
@@ -852,7 +852,7 @@ class BasePaymentProvider:
|
||||
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
|
||||
return False
|
||||
|
||||
if order.sales_channel.identifier not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
||||
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
||||
return False
|
||||
|
||||
return self._is_available_by_time(order=order)
|
||||
@@ -1185,14 +1185,14 @@ class ManualPayment(BasePaymentProvider):
|
||||
label=_('Payment process description during checkout'),
|
||||
help_text=_('This text will be shown during checkout when the user selects this payment method. '
|
||||
'It should give a short explanation on this payment method.'),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
)),
|
||||
('email_instructions', I18nFormField(
|
||||
label=_('Payment process description in order confirmation emails'),
|
||||
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
|
||||
'mails. It should instruct the user on how to proceed with the payment. You can use '
|
||||
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||
)),
|
||||
('pending_description', I18nFormField(
|
||||
@@ -1200,7 +1200,7 @@ class ManualPayment(BasePaymentProvider):
|
||||
help_text=_('This text will be shown on the order confirmation page for pending orders. '
|
||||
'It should instruct the user on how to proceed with the payment. You can use '
|
||||
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||
)),
|
||||
('invoice_immediately',
|
||||
@@ -1325,7 +1325,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
(
|
||||
"public_description",
|
||||
I18nFormField(
|
||||
label=_("Payment method description"), widget=I18nMarkdownTextarea, required=False
|
||||
label=_("Payment method description"), widget=I18nTextarea, required=False
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1441,7 +1441,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
if not gc.testmode and self.event.testmode:
|
||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||
return
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
if gc.expires and gc.expires < now():
|
||||
messages.error(request, _("This gift card is no longer valid."))
|
||||
return
|
||||
if gc.value <= Decimal("0.00"):
|
||||
@@ -1491,7 +1491,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
if not gc.testmode and payment.order.testmode:
|
||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||
return
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
if gc.expires and gc.expires < now():
|
||||
messages.error(request, _("This gift card is no longer valid."))
|
||||
return
|
||||
if gc.value <= Decimal("0.00"):
|
||||
@@ -1539,7 +1539,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
raise PaymentException(_("This gift card can only be used in test mode."))
|
||||
if not gc.testmode and payment.order.testmode:
|
||||
raise PaymentException(_("Only test gift cards can be used in test mode."))
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
if gc.expires and gc.expires < now():
|
||||
raise PaymentException(_("This gift card is no longer valid."))
|
||||
|
||||
trans = gc.transactions.create(
|
||||
|
||||
@@ -62,7 +62,8 @@ from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf import PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
@@ -407,30 +408,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"TIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_date", {
|
||||
"label": _("Purchase date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"SHORT_DATE_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_datetime", {
|
||||
"label": _("Purchase date and time"),
|
||||
"editor_sample": _("2017-05-31 19:00"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"SHORT_DATETIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_time", {
|
||||
"label": _("Purchase time"),
|
||||
"editor_sample": _("19:00"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"TIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("valid_from_date", {
|
||||
"label": _("Validity start date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
@@ -1068,81 +1045,56 @@ class Renderer:
|
||||
canvas.showPage()
|
||||
|
||||
def render_background(self, buffer, title=_('Ticket')):
|
||||
buffer.seek(0)
|
||||
fg_pdf = PdfReader(buffer)
|
||||
|
||||
if settings.PDFTK:
|
||||
buffer.seek(0)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fg_filename = os.path.join(d, 'fg.pdf')
|
||||
bg_filename = os.path.join(d, 'bg.pdf')
|
||||
out_filename = os.path.join(d, 'out.pdf')
|
||||
|
||||
buffer.seek(0)
|
||||
with open(fg_filename, 'wb') as f:
|
||||
with open(os.path.join(d, 'back.pdf'), 'wb') as f:
|
||||
f.write(self.bg_bytes)
|
||||
with open(os.path.join(d, 'front.pdf'), 'wb') as f:
|
||||
f.write(buffer.read())
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||
# than foreground-PDF, we stamp front onto back instead.
|
||||
# Just changing PDF-version in fg.pdf to match the version of
|
||||
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||
#
|
||||
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||
# note: self.bg_pdf is a PdfReader(), not a PdfWriter()
|
||||
fg_num_pages = fg_pdf.get_num_pages()
|
||||
bg_num_pages = self.bg_pdf.get_num_pages()
|
||||
bg_pdf_to_merge = PdfWriter()
|
||||
bg_pdf_to_merge.append(
|
||||
self.bg_pdf,
|
||||
pages=(0, min(bg_num_pages, fg_num_pages)),
|
||||
import_outline=False,
|
||||
excluded_fields=("/Annots", "/B")
|
||||
)
|
||||
if fg_num_pages > bg_num_pages:
|
||||
# repeat last page in bg_pdf to match fg_pdf
|
||||
bg_pdf_to_merge.append(
|
||||
bg_pdf_to_merge,
|
||||
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
|
||||
import_outline=False,
|
||||
excluded_fields=("/Annots", "/B")
|
||||
)
|
||||
|
||||
bg_pdf_to_merge.write(bg_filename)
|
||||
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
bg_filename,
|
||||
'multistamp',
|
||||
fg_filename
|
||||
]
|
||||
|
||||
else:
|
||||
with open(bg_filename, 'wb') as f:
|
||||
f.write(self.bg_bytes)
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename
|
||||
]
|
||||
|
||||
pdftk_cmd.extend(('output', out_filename, 'compress'))
|
||||
subprocess.run(pdftk_cmd, check=True)
|
||||
with open(out_filename, 'rb') as f:
|
||||
subprocess.run([
|
||||
settings.PDFTK,
|
||||
os.path.join(d, 'front.pdf'),
|
||||
'multibackground',
|
||||
os.path.join(d, 'back.pdf'),
|
||||
'output',
|
||||
os.path.join(d, 'out.pdf'),
|
||||
'compress'
|
||||
], check=True)
|
||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
||||
return BytesIO(f.read())
|
||||
else:
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfReader(buffer)
|
||||
output = PdfWriter()
|
||||
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = self.bg_pdf.pages[i]
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
page.merge_page(bg_page, over=False)
|
||||
output.add_page(page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
output.pdf_header = self.bg_pdf.pdf_header
|
||||
for i, page in enumerate(new_pdf.pages):
|
||||
bg_page = copy.deepcopy(self.bg_pdf.pages[i])
|
||||
bg_rotation = bg_page.get('/Rotate')
|
||||
if bg_rotation:
|
||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
||||
t = Transformation().rotate(bg_rotation)
|
||||
w = float(page.mediabox.getWidth())
|
||||
h = float(page.mediabox.getHeight())
|
||||
if bg_rotation in (90, 270):
|
||||
# offset due to rotation base
|
||||
if bg_rotation == 90:
|
||||
t = t.translate(h, 0)
|
||||
else:
|
||||
t = t.translate(0, w)
|
||||
# rotate mediabox as well
|
||||
page.mediabox = RectangleObject((
|
||||
page.mediabox.left.as_numeric(),
|
||||
page.mediabox.bottom.as_numeric(),
|
||||
page.mediabox.top.as_numeric(),
|
||||
page.mediabox.right.as_numeric(),
|
||||
))
|
||||
page.trimbox = page.mediabox
|
||||
elif bg_rotation == 180:
|
||||
t = t.translate(w, h)
|
||||
page.add_transformation(t)
|
||||
bg_page.merge_page(page)
|
||||
output.add_page(bg_page)
|
||||
|
||||
output.add_metadata({
|
||||
'/Title': str(title),
|
||||
@@ -1154,66 +1106,54 @@ class Renderer:
|
||||
return outbuffer
|
||||
|
||||
|
||||
def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
||||
def merge_background(fg_pdf, bg_pdf, out_file, compress):
|
||||
if settings.PDFTK:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fg_filename = os.path.join(d, 'fg.pdf')
|
||||
bg_filename = os.path.join(d, 'bg.pdf')
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||
# than foreground-PDF, we stamp front onto back instead.
|
||||
# Just changing PDF-version in fg.pdf to match the version of
|
||||
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||
|
||||
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||
fg_num_pages = fg_pdf.get_num_pages()
|
||||
bg_num_pages = bg_pdf.get_num_pages()
|
||||
if fg_num_pages > bg_num_pages:
|
||||
# repeat last page in bg_pdf to match fg_pdf
|
||||
bg_pdf.append(
|
||||
bg_pdf,
|
||||
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
|
||||
import_outline=False,
|
||||
excluded_fields=("/Annots", "/B")
|
||||
)
|
||||
|
||||
bg_pdf.write(bg_filename)
|
||||
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
bg_filename,
|
||||
'multistamp',
|
||||
fg_filename,
|
||||
]
|
||||
else:
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename
|
||||
]
|
||||
|
||||
pdftk_cmd.extend(('output', '-'))
|
||||
if compress:
|
||||
pdftk_cmd.append('compress')
|
||||
|
||||
fg_pdf.write(fg_filename)
|
||||
bg_pdf.write(bg_filename)
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename,
|
||||
'output',
|
||||
'-',
|
||||
]
|
||||
if compress:
|
||||
pdftk_cmd.append('compress')
|
||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||
else:
|
||||
output = PdfWriter()
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = bg_pdf.pages[i]
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
page.merge_page(bg_page, over=False)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||
|
||||
fg_pdf.write(out_file)
|
||||
bg_page = copy.deepcopy(bg_pdf.pages[i])
|
||||
bg_rotation = bg_page.get('/Rotate')
|
||||
if bg_rotation:
|
||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
||||
t = Transformation().rotate(bg_rotation)
|
||||
w = float(page.mediabox.getWidth())
|
||||
h = float(page.mediabox.getHeight())
|
||||
if bg_rotation in (90, 270):
|
||||
# offset due to rotation base
|
||||
if bg_rotation == 90:
|
||||
t = t.translate(h, 0)
|
||||
else:
|
||||
t = t.translate(0, w)
|
||||
# rotate mediabox as well
|
||||
page.mediabox = RectangleObject((
|
||||
page.mediabox.left.as_numeric(),
|
||||
page.mediabox.bottom.as_numeric(),
|
||||
page.mediabox.top.as_numeric(),
|
||||
page.mediabox.right.as_numeric(),
|
||||
))
|
||||
page.trimbox = page.mediabox
|
||||
elif bg_rotation == 180:
|
||||
t = t.translate(w, h)
|
||||
page.add_transformation(t)
|
||||
bg_page.merge_page(page)
|
||||
output.add_page(bg_page)
|
||||
output.write(out_file)
|
||||
|
||||
|
||||
@deconstructible
|
||||
|
||||
@@ -257,8 +257,6 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
|
||||
kwargs['valid_from'] = position.valid_from
|
||||
if 'valid_until' in params:
|
||||
kwargs['valid_until'] = position.valid_until
|
||||
if 'order_datetime' in params:
|
||||
kwargs['order_datetime'] = position.order.datetime
|
||||
secret = gen.generate_secret(
|
||||
item=position.item,
|
||||
variation=position.variation,
|
||||
|
||||
@@ -52,11 +52,12 @@ from django.utils.translation import (
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, SalesChannel,
|
||||
Seat, SeatCategoryMapping, Voucher,
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
|
||||
SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
@@ -73,7 +74,6 @@ from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
|
||||
from pretix.base.signals import validate_cart_addons
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||
from pretix.celery_app import app
|
||||
from pretix.presale.signals import (
|
||||
checkout_confirm_messages, fee_calculation_for_cart,
|
||||
@@ -274,11 +274,11 @@ class CartManager:
|
||||
AddOperation: 30
|
||||
}
|
||||
|
||||
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
|
||||
invoice_address: InvoiceAddress=None, widget_data=None):
|
||||
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
|
||||
sales_channel='web'):
|
||||
self.event = event
|
||||
self.cart_id = cart_id
|
||||
self.real_now_dt = now()
|
||||
self.now_dt = now()
|
||||
self._operations = []
|
||||
self._quota_diff = Counter()
|
||||
self._voucher_use_diff = Counter()
|
||||
@@ -305,10 +305,10 @@ class CartManager:
|
||||
return self._seated_cache[item, subevent]
|
||||
|
||||
def _calculate_expiry(self):
|
||||
self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||
|
||||
def _check_presale_dates(self):
|
||||
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
||||
if self.event.presale_start and self.now_dt < self.event.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
if self.event.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
@@ -319,13 +319,13 @@ class CartManager:
|
||||
tlv.datetime(self.event).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < time_machine_now(self.real_now_dt):
|
||||
if term_last < self.now_dt:
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
||||
# We can extend the reservation of items which are not yet expired without risk
|
||||
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
|
||||
self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry)
|
||||
|
||||
def _delete_out_of_timeframe(self):
|
||||
err = None
|
||||
@@ -333,12 +333,12 @@ class CartManager:
|
||||
if not cp.pk:
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and time_machine_now(self.real_now_dt) < cp.subevent.presale_start:
|
||||
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
|
||||
err = error_messages['some_subevent_not_started']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
|
||||
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||
if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end:
|
||||
err = error_messages['some_subevent_ended']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
@@ -350,7 +350,7 @@ class CartManager:
|
||||
tlv.datetime(cp.subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < time_machine_now(self.real_now_dt):
|
||||
if term_last < self.now_dt:
|
||||
err = error_messages['some_subevent_ended']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
@@ -383,7 +383,7 @@ class CartManager:
|
||||
})
|
||||
|
||||
def _check_max_cart_size(self):
|
||||
if not self._sales_channel.type_instance.unlimited_items_per_order:
|
||||
if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
|
||||
cartsize = self.positions.filter(addon_to__isnull=True).count()
|
||||
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
|
||||
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
|
||||
@@ -421,13 +421,8 @@ class CartManager:
|
||||
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||
raise CartError(error_messages['media_usage_not_implemented'])
|
||||
|
||||
if not op.item.all_sales_channels:
|
||||
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.variation and not op.variation.all_sales_channels:
|
||||
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
@@ -454,21 +449,14 @@ class CartManager:
|
||||
if op.subevent and not op.subevent.active:
|
||||
raise CartError(error_messages['inactive_subevent'])
|
||||
|
||||
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
|
||||
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
|
||||
if op.subevent and op.subevent.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
seated = self._is_seated(op.item, op.subevent)
|
||||
if (
|
||||
seated and (
|
||||
not op.seat or (
|
||||
op.seat.blocked and
|
||||
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
|
||||
)
|
||||
)
|
||||
):
|
||||
if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and not seated:
|
||||
raise CartError(error_messages['seat_forbidden'])
|
||||
@@ -484,7 +472,7 @@ class CartManager:
|
||||
tlv.datetime(op.subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < time_machine_now(self.real_now_dt):
|
||||
if term_last < self.now_dt:
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
@@ -521,7 +509,7 @@ class CartManager:
|
||||
)
|
||||
if not self.event.settings.seating_choice:
|
||||
requires_seat = Value(0, output_field=IntegerField())
|
||||
expired = self.positions.filter(expires__lte=self.real_now_dt).select_related(
|
||||
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
|
||||
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
|
||||
).annotate(
|
||||
requires_seat=requires_seat
|
||||
@@ -702,7 +690,7 @@ class CartManager:
|
||||
# than either of the possible default assumptions.
|
||||
predicted_redeemed_after = (
|
||||
voucher.redeemed +
|
||||
CartPosition.objects.filter(voucher=voucher, expires__gte=self.real_now_dt).count() +
|
||||
CartPosition.objects.filter(voucher=voucher, expires__gte=self.now_dt).count() +
|
||||
self._voucher_use_diff[voucher] +
|
||||
voucher_use_diff[voucher]
|
||||
)
|
||||
@@ -994,7 +982,7 @@ class CartManager:
|
||||
current_num = len(current_addons[cp].get(k, []))
|
||||
if input_num < current_num:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.expires > self.real_now_dt:
|
||||
if a.expires > self.now_dt:
|
||||
quotas = list(a.quotas)
|
||||
|
||||
for quota in quotas:
|
||||
@@ -1008,7 +996,7 @@ class CartManager:
|
||||
|
||||
def _get_voucher_availability(self):
|
||||
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
|
||||
self.event, self._voucher_use_diff, self.real_now_dt,
|
||||
self.event, self._voucher_use_diff, self.now_dt,
|
||||
exclude_position_ids=[
|
||||
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
|
||||
]
|
||||
@@ -1113,7 +1101,7 @@ class CartManager:
|
||||
shared_lock_objects=[self.event]
|
||||
)
|
||||
vouchers_ok = self._get_voucher_availability()
|
||||
quotas_ok = _get_quota_availability(self._quota_diff, self.real_now_dt)
|
||||
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
|
||||
err = None
|
||||
new_cart_positions = []
|
||||
deleted_positions = set()
|
||||
@@ -1130,7 +1118,7 @@ class CartManager:
|
||||
|
||||
for iop, op in enumerate(self._operations):
|
||||
if isinstance(op, self.RemoveOperation):
|
||||
if op.position.expires > self.real_now_dt:
|
||||
if op.position.expires > self.now_dt:
|
||||
for q in op.position.quotas:
|
||||
quotas_ok[q] += 1
|
||||
addons = op.position.addons.all()
|
||||
@@ -1382,7 +1370,7 @@ class CartManager:
|
||||
|
||||
discount_results = apply_discounts(
|
||||
self.event,
|
||||
self._sales_channel.identifier,
|
||||
self._sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in positions
|
||||
@@ -1407,7 +1395,7 @@ class CartManager:
|
||||
err = self.extend_expired_positions() or err
|
||||
err = err or self._check_min_per_voucher()
|
||||
|
||||
self.real_now_dt = now()
|
||||
self.now_dt = now()
|
||||
|
||||
self._extend_expiry_of_valid_existing_positions()
|
||||
err = self._perform_operations() or err
|
||||
@@ -1499,7 +1487,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, widget_data=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
|
||||
"""
|
||||
Adds a list of items to a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -1507,7 +1495,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
:param cart_id: Session ID of a guest
|
||||
:raises CartError: On any error that occurred
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
with language(locale):
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
@@ -1516,11 +1504,6 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
|
||||
@@ -1534,18 +1517,14 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param voucher: A voucher code
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
@@ -1558,18 +1537,14 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param position: A cart position ID
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
@@ -1582,17 +1557,13 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
@@ -1606,14 +1577,14 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
invoice_address: int=None, sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param addons: A list of dicts with the keys addon_to, item, variation
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
with language(locale):
|
||||
ia = False
|
||||
if invoice_address:
|
||||
try:
|
||||
@@ -1621,10 +1592,6 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
|
||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
||||
|
||||
@@ -42,8 +42,8 @@ from dateutil.tz import datetime_exists
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import (
|
||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||
OuterRef, Q, Subquery, TextField, Value, When,
|
||||
BooleanField, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||
OuterRef, Q, Subquery, Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, TruncDate
|
||||
from django.dispatch import receiver
|
||||
@@ -273,14 +273,6 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
var_texts[vname] = _('Only allowed before {datetime}').format(datetime=compare_to_text)
|
||||
elif operator == 'isAfter':
|
||||
var_texts[vname] = _('Only allowed after {datetime}').format(datetime=compare_to_text)
|
||||
elif var == 'entry_status':
|
||||
var_weights[vname] = (20, 0)
|
||||
if operator == '==' and rhs[0] == 'present':
|
||||
var_texts[vname] = _('Attendee is checked out')
|
||||
elif operator == '==' and rhs[0] == 'absent':
|
||||
var_texts[vname] = _('Attendee is already checked in')
|
||||
else:
|
||||
var_texts[vname] = f'{var} not {operator} {rhs}'
|
||||
elif var == 'product' or var == 'variation':
|
||||
var_weights[vname] = (1000, 0)
|
||||
var_texts[vname] = _('Ticket type not allowed')
|
||||
@@ -515,13 +507,6 @@ class LazyRuleVars:
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('day').distinct().count()
|
||||
|
||||
@cached_property
|
||||
def entry_status(self):
|
||||
last_checkin = self._position.checkins.filter(list=self._clist).order_by('datetime').last()
|
||||
if not last_checkin or last_checkin.type == Checkin.TYPE_EXIT:
|
||||
return "absent"
|
||||
return "present"
|
||||
|
||||
@cached_property
|
||||
def minutes_since_last_entry(self):
|
||||
tz = self._clist.event.timezone
|
||||
@@ -584,8 +569,6 @@ class SQLLogic:
|
||||
'entries_days_since', 'entries_days_before'}
|
||||
|
||||
def operation_to_expression(self, rule):
|
||||
if isinstance(rule, str):
|
||||
return Value(rule)
|
||||
if not isinstance(rule, dict):
|
||||
return rule
|
||||
|
||||
@@ -787,25 +770,6 @@ class SQLLogic:
|
||||
Value(-1),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif values[0] == 'entry_status':
|
||||
sq_last_checkin = Subquery(
|
||||
Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=self.list.pk,
|
||||
).order_by('-datetime').values('type')[:1]
|
||||
)
|
||||
|
||||
return Case(
|
||||
When(
|
||||
condition=Equal(
|
||||
sq_last_checkin,
|
||||
Value(Checkin.TYPE_ENTRY)
|
||||
),
|
||||
then=Value("present"),
|
||||
),
|
||||
default=Value("absent"),
|
||||
output_field=TextField()
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unknown operator {operator}')
|
||||
|
||||
@@ -1159,7 +1123,7 @@ def order_placed(sender, **kwargs):
|
||||
order = kwargs['order']
|
||||
event = sender
|
||||
|
||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
|
||||
'limit_products'))
|
||||
if not cls:
|
||||
return
|
||||
|
||||
@@ -420,7 +420,7 @@ def invoice_pdf_task(invoice: int):
|
||||
|
||||
def invoice_qualified(order: Order):
|
||||
if order.total == Decimal('0.00') or order.require_approval or \
|
||||
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
|
||||
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -443,11 +443,8 @@ def build_preview_invoice_pdf(event):
|
||||
locale = event.settings.locale
|
||||
|
||||
with rolledback_transaction(), language(locale, event.settings.region):
|
||||
order = event.orders.create(
|
||||
status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count(),
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
)
|
||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
|
||||
invoice = Invoice(
|
||||
order=order, event=event, invoice_no="PREVIEW",
|
||||
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
||||
|
||||
@@ -470,8 +470,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
# just "ABC-123.pdf", but we only do so if our currently selected language allows to do this
|
||||
# as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this
|
||||
# has shown to cause deliverability problems of the email and deliverability wins.
|
||||
with language(order.locale if order else inv.locale, event.settings.region if event else None):
|
||||
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
|
||||
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
|
||||
if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename):
|
||||
filename = inv.number.replace(' ', '_') + '.pdf'
|
||||
filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename)
|
||||
|
||||
@@ -25,13 +25,13 @@ from typing import List, Optional
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
|
||||
OrderPosition, SubEvent,
|
||||
)
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
|
||||
|
||||
else:
|
||||
# Always start at start of day
|
||||
date_start = time_machine_now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
date_start = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
date_end = date_start
|
||||
|
||||
if item.grant_membership_duration_months:
|
||||
|
||||
@@ -213,8 +213,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
||||
voucher = Voucher(event=event)
|
||||
vouchers.append(voucher)
|
||||
|
||||
if not record.get("code"):
|
||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||
Voucher.clean_item_properties(
|
||||
record,
|
||||
event,
|
||||
|
||||
@@ -62,6 +62,7 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import OAuthApplication
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import get_language_without_region, language
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
@@ -75,7 +76,7 @@ from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
@@ -98,10 +99,9 @@ from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
||||
order_fee_calculation, order_paid, order_placed, order_reactivated,
|
||||
order_split, order_valid_if_pending, periodic_task, validate_order,
|
||||
order_fee_calculation, order_paid, order_placed, order_split,
|
||||
order_valid_if_pending, periodic_task, validate_order,
|
||||
)
|
||||
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -199,7 +199,6 @@ error_messages = {
|
||||
),
|
||||
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
|
||||
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
|
||||
'currency_XXX': gettext_lazy('Paid products not supported without a valid currency.'),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -253,7 +252,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
||||
else:
|
||||
raise OrderError(is_available)
|
||||
|
||||
order_reactivated.send(order.event, order=order)
|
||||
order_approved.send(order.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
@@ -298,7 +297,6 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
|
||||
auth=auth,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
'force': force,
|
||||
'state_change': was_expired
|
||||
}
|
||||
)
|
||||
@@ -468,10 +466,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
order_denied.send(order.event, order=order)
|
||||
|
||||
if send_mail:
|
||||
email_template = order.event.settings.mail_text_order_denied
|
||||
email_subject = order.event.settings.mail_subject_order_denied
|
||||
email_context = get_email_context(event=order.event, order=order, comment=comment)
|
||||
with language(order.locale, order.event.settings.region):
|
||||
email_template = order.event.settings.mail_text_order_denied
|
||||
email_subject = order.event.settings.mail_subject_order_denied
|
||||
email_context = get_email_context(event=order.event, order=order, comment=comment)
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
@@ -648,10 +646,10 @@ def _check_date(event: Event, now_dt: datetime):
|
||||
raise OrderError(error_messages['ended'])
|
||||
|
||||
|
||||
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
|
||||
sales_channel: SalesChannel, address: InvoiceAddress=None, customer=None):
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
|
||||
sales_channel='web', customer=None):
|
||||
err = None
|
||||
_check_date(event, time_machine_now_dt)
|
||||
_check_date(event, now_dt)
|
||||
|
||||
products_seen = Counter()
|
||||
q_avail = Counter()
|
||||
@@ -729,7 +727,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
|
||||
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
|
||||
err = err or error_messages['some_subevent_not_started']
|
||||
delete(cp)
|
||||
break
|
||||
@@ -741,7 +739,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
tlv.datetime(cp.subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), event.timezone)
|
||||
if term_last < time_machine_now_dt:
|
||||
if term_last < now_dt:
|
||||
err = err or error_messages['some_subevent_ended']
|
||||
delete(cp)
|
||||
break
|
||||
@@ -773,7 +771,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
if cp.seat:
|
||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
||||
# time, since we absolutely can not overbook a seat.
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
|
||||
err = err or error_messages['seat_unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
@@ -787,19 +785,19 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
|
||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
|
||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt:
|
||||
err = err or error_messages['voucher_expired']
|
||||
delete(cp)
|
||||
continue
|
||||
@@ -871,7 +869,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||
discount_results = apply_discounts(
|
||||
event,
|
||||
sales_channel.identifier,
|
||||
sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in sorted_positions
|
||||
@@ -957,11 +955,12 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
|
||||
return fees
|
||||
|
||||
|
||||
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
|
||||
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
|
||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
||||
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
||||
customer=None, valid_if_pending=False):
|
||||
payments = []
|
||||
sales_channel = get_all_sales_channels()[sales_channel]
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
||||
@@ -983,10 +982,10 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
||||
datetime=now_dt,
|
||||
locale=get_language_without_region(locale),
|
||||
total=total,
|
||||
testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False,
|
||||
testmode=True if sales_channel.testmode_supported and event.testmode else False,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
require_approval=require_approval,
|
||||
sales_channel=sales_channel,
|
||||
sales_channel=sales_channel.identifier,
|
||||
customer=customer,
|
||||
valid_if_pending=valid_if_pending,
|
||||
)
|
||||
@@ -1105,11 +1104,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
if customer:
|
||||
customer = event.organizer.customers.get(pk=customer)
|
||||
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise OrderError("Invalid sales channel.")
|
||||
|
||||
if email == settings.PRETIX_EMAIL_NONE_VALUE:
|
||||
email = None
|
||||
|
||||
@@ -1135,9 +1129,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
id__in=position_ids, event=event
|
||||
)
|
||||
|
||||
if shown_total is not None and Decimal(shown_total) > Decimal("0.00") and event.currency == "XXX":
|
||||
raise OrderError(error_messages['currency_XXX'])
|
||||
|
||||
validate_order.send(
|
||||
event,
|
||||
payment_provider=payment_requests[0]['provider'] if payment_requests else None, # only for backwards compatibility
|
||||
@@ -1167,8 +1158,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
warnings = []
|
||||
any_payment_failed = False
|
||||
|
||||
real_now_dt = now()
|
||||
time_machine_now_dt = time_machine_now(real_now_dt)
|
||||
now_dt = now()
|
||||
err_out = None
|
||||
with transaction.atomic(durable=True):
|
||||
positions = list(
|
||||
@@ -1180,28 +1170,16 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
try:
|
||||
_check_positions(event, real_now_dt, time_machine_now_dt, positions,
|
||||
address=addr, sales_channel=sales_channel, customer=customer)
|
||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
||||
except OrderError as e:
|
||||
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
|
||||
else:
|
||||
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||
sleep(2)
|
||||
|
||||
order, payment_objs = _create_order(
|
||||
event,
|
||||
email=email,
|
||||
positions=positions,
|
||||
now_dt=real_now_dt,
|
||||
payment_requests=payment_requests,
|
||||
locale=locale,
|
||||
address=addr,
|
||||
meta_info=meta_info,
|
||||
sales_channel=sales_channel,
|
||||
shown_total=shown_total,
|
||||
customer=customer,
|
||||
valid_if_pending=valid_if_pending
|
||||
)
|
||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
||||
|
||||
try:
|
||||
for p in payment_objs:
|
||||
@@ -1285,7 +1263,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
||||
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
|
||||
|
||||
if sales_channel.identifier in event.settings.mail_sales_channel_placed_paid:
|
||||
if sales_channel in event.settings.mail_sales_channel_placed_paid:
|
||||
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
|
||||
is_free=free_order_flow)
|
||||
if email_attendees:
|
||||
@@ -1437,7 +1415,7 @@ def send_download_reminders(sender, **kwargs):
|
||||
if days is None:
|
||||
continue
|
||||
|
||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||
if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
|
||||
continue
|
||||
|
||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
@@ -1939,13 +1917,9 @@ class OrderChangeManager:
|
||||
if not item.is_available() or (variation and not variation.is_available()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if not item.all_sales_channels:
|
||||
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if variation and not variation.all_sales_channels:
|
||||
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
if self.order.sales_channel not in item.sales_channels or (
|
||||
variation and self.order.sales_channel not in variation.sales_channels):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
@@ -2034,23 +2008,6 @@ class OrderChangeManager:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
is_unavailable = (
|
||||
# If an item is no longer available due to time, it should usually also be no longer
|
||||
# user-removable, because e.g. the stock has already been ordered.
|
||||
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
||||
# not mean it should be unremovable for others.
|
||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||
# be part of the input.
|
||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||
or (
|
||||
not item.all_sales_channels and
|
||||
not item.limit_sales_channels.contains(self.order.sales_channel)
|
||||
)
|
||||
)
|
||||
if is_unavailable:
|
||||
continue
|
||||
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
||||
raise OrderError(
|
||||
error_messages['addon_already_checked_in'] % {
|
||||
@@ -2168,9 +2125,6 @@ class OrderChangeManager:
|
||||
)
|
||||
|
||||
def _check_paid_to_free(self):
|
||||
if self.event.currency == 'XXX' and self.order.total + self._totaldiff > Decimal("0.00"):
|
||||
raise OrderError(error_messages['currency_XXX'])
|
||||
|
||||
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval:
|
||||
if not self.order.fees.exists() and not self.order.positions.exists():
|
||||
# The order is completely empty now, so we cancel it.
|
||||
@@ -2873,8 +2827,8 @@ class OrderChangeManager:
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
sales_channel: str='web', shown_total=None, customer=None):
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||
|
||||
@@ -337,40 +337,6 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.modify', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.modify', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.change', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.change', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||
lambda order, event:
|
||||
|
||||
@@ -25,15 +25,14 @@ from typing import List, Optional, Tuple
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
|
||||
SalesChannel, Voucher,
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import Event, SubEvent
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
|
||||
|
||||
def get_price(item: Item, variation: ItemVariation = None,
|
||||
@@ -165,14 +164,12 @@ def apply_discounts(event: Event, sales_channel: str,
|
||||
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
||||
"""
|
||||
if isinstance(sales_channel, SalesChannel):
|
||||
sales_channel = sales_channel.identifier
|
||||
new_prices = {}
|
||||
|
||||
discount_qs = event.discounts.filter(
|
||||
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
|
||||
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
|
||||
Q(available_from__isnull=True) | Q(available_from__lte=now()),
|
||||
Q(available_until__isnull=True) | Q(available_until__gte=now()),
|
||||
sales_channels__contains=sales_channel,
|
||||
active=True,
|
||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||
for discount in discount_qs:
|
||||
|
||||
@@ -29,7 +29,6 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
prefetch_related_objects,
|
||||
)
|
||||
from django.utils.timezone import now
|
||||
|
||||
@@ -447,7 +446,6 @@ class QuotaAvailability:
|
||||
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
|
||||
|
||||
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
|
||||
prefetch_related_objects(quotas, "event", "event__organizer")
|
||||
quotas = [
|
||||
q for q in quotas
|
||||
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
|
||||
|
||||
@@ -62,10 +62,7 @@ class VATIDTemporaryError(VATIDError):
|
||||
|
||||
def _validate_vat_id_NO(vat_id, country_code):
|
||||
# Inspired by vat_moss library
|
||||
try:
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
except ValueError:
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
|
||||
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
@@ -105,7 +105,6 @@ def preview(event: int, provider: str):
|
||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||
email='sample@pretix.eu',
|
||||
locale=event.settings.locale,
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
expires=now(), code="PREVIEW1234", total=119)
|
||||
|
||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import transaction
|
||||
@@ -50,28 +49,19 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
|
||||
quota_cache = {}
|
||||
gone = set()
|
||||
_seats_available_cache = {}
|
||||
seats_used = defaultdict(int)
|
||||
seats_available = {}
|
||||
|
||||
seated_product_set = set(
|
||||
SeatCategoryMapping.objects.filter(event=event).values_list('product_id', 'subevent_id')
|
||||
)
|
||||
|
||||
def _seats_available(item, subevent):
|
||||
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'):
|
||||
# See comment in WaitingListEntry.send_voucher() for rationale
|
||||
subevent_id = subevent.pk if subevent else None
|
||||
if (item.pk, subevent_id) not in _seats_available_cache:
|
||||
num_free_seats_for_product = (subevent or event).free_seats().filter(product_id=item.pk).count()
|
||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=item.pk,
|
||||
subevent_id=subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
_seats_available_cache[item.pk, subevent_id] = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
|
||||
return _seats_available_cache[item.pk, subevent_id] - seats_used[item.pk, subevent_id]
|
||||
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count()
|
||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=m.product_id,
|
||||
subevent_id=m.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
|
||||
|
||||
prefetch_related_objects(
|
||||
[event.organizer],
|
||||
@@ -113,7 +103,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
|
||||
lock_objects(quotas, shared_lock_objects=[event])
|
||||
for wle in qs:
|
||||
if (wle.item_id, wle.variation_id, wle.subevent_id) in gone:
|
||||
if (wle.item, wle.variation, wle.subevent) in gone:
|
||||
continue
|
||||
ev = (wle.subevent or event)
|
||||
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
||||
@@ -121,15 +111,15 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
if wle.subevent and not wle.subevent.presale_is_running:
|
||||
continue
|
||||
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
if not wle.item.is_available():
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||
if _seats_available(wle.item, wle.subevent) < 1:
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
if seats_available[wle.item_id, wle.subevent_id] < 1:
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
availability = (
|
||||
@@ -151,10 +141,10 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
||||
)
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||
seats_used[wle.item_id, wle.subevent_id] += 1
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
seats_available[wle.item_id, wle.subevent_id] -= 1
|
||||
else:
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
|
||||
return sent
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ from pretix.api.serializers.fields import (
|
||||
ListMultipleChoiceField, UploadedFileField,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nField, I18nURLField
|
||||
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
|
||||
from pretix.base.forms import I18nURLFormField
|
||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
|
||||
from pretix.base.reldate import (
|
||||
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
|
||||
@@ -602,7 +602,7 @@ DEFAULTS = {
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Invoice address explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above the invoice address form during checkout.")
|
||||
)
|
||||
@@ -801,7 +801,7 @@ DEFAULTS = {
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
label=_("End of presale text"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
|
||||
"is over. You can use it to describe other options to get a ticket, such as a box office.")
|
||||
@@ -813,7 +813,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': 3,
|
||||
}},
|
||||
@@ -971,8 +971,7 @@ DEFAULTS = {
|
||||
},
|
||||
'payment_giftcard__enabled': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'type': bool
|
||||
},
|
||||
'payment_giftcard_public_name': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
|
||||
@@ -1460,7 +1459,7 @@ DEFAULTS = {
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Phone number explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
||||
)
|
||||
@@ -1654,28 +1653,6 @@ DEFAULTS = {
|
||||
"calendar.")
|
||||
)
|
||||
},
|
||||
'allow_modifications': {
|
||||
'default': 'order',
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=(
|
||||
('no', _('No modifications after order was submitted')),
|
||||
('order', _('Only the person who ordered can make changes')),
|
||||
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||
)
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to modify their information"),
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('no', _('No modifications after order was submitted')),
|
||||
('order', _('Only the person who ordered can make changes')),
|
||||
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||
)
|
||||
),
|
||||
},
|
||||
'allow_modifications_after_checkin': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1683,8 +1660,6 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to modify their information after they checked in."),
|
||||
help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
|
||||
"in the order has been checked in.")
|
||||
)
|
||||
},
|
||||
'last_order_modification_date': {
|
||||
@@ -1901,7 +1876,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Voluntary lower refund explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
|
||||
"which your customers can use to choose the amount they would like to receive. You can use it "
|
||||
@@ -1983,7 +1958,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Terms of cancellation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown when cancellation is allowed for a paid order. Leave empty if you "
|
||||
"want pretix to automatically generate the terms of cancellation based on your settings.")
|
||||
@@ -1996,7 +1971,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Terms of cancellation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown when cancellation is allowed for an unpaid or free order. Leave empty "
|
||||
"if you want pretix to automatically generate the terms of cancellation based on your settings.")
|
||||
@@ -2085,7 +2060,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Event description"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
help_text=_(
|
||||
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
|
||||
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
|
||||
@@ -2851,6 +2826,22 @@ Your {organizer} team""")) # noqa: W291
|
||||
**primary_font_kwargs()
|
||||
),
|
||||
},
|
||||
'presale_css_file': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'presale_css_checksum': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'presale_widget_css_file': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'presale_widget_css_checksum': {
|
||||
'default': None,
|
||||
'type': str
|
||||
},
|
||||
'logo_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
@@ -3000,7 +2991,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Frontpage text"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'event_info_text': {
|
||||
@@ -3022,7 +3013,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Banner text (top)"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above every page of your shop. Please only use this for "
|
||||
"very important messages.")
|
||||
@@ -3035,7 +3026,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Banner text (bottom)"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown below every page of your shop. Please only use this for "
|
||||
"very important messages.")
|
||||
@@ -3048,7 +3039,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Voucher explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
|
||||
"how to obtain a voucher code.")
|
||||
@@ -3061,7 +3052,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Attendee data explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain "
|
||||
"why you need information from them.")
|
||||
@@ -3077,7 +3068,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
|
||||
"to the default text."),
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'checkout_phone_helptext': {
|
||||
@@ -3088,7 +3079,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_kwargs': dict(
|
||||
label=_("Help text of the phone number field"),
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'checkout_email_helptext': {
|
||||
@@ -3102,7 +3093,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_kwargs': dict(
|
||||
label=_("Help text of the email field"),
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'order_import_settings': {
|
||||
@@ -3232,7 +3223,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Homepage text'),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
help_text=_('This will be displayed on the organizer homepage.')
|
||||
)
|
||||
},
|
||||
@@ -3289,7 +3280,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Dialog text"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
@@ -3304,7 +3295,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Secondary dialog text"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
|
||||
)
|
||||
},
|
||||
@@ -3380,6 +3371,10 @@ Your {organizer} team""")) # noqa: W291
|
||||
'type': str,
|
||||
}
|
||||
}
|
||||
SETTINGS_AFFECTING_CSS = {
|
||||
'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
|
||||
'theme_color_background', 'theme_round_borders'
|
||||
}
|
||||
PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||
('english_common', (_('Most common English titles'), (
|
||||
'Mr',
|
||||
@@ -3424,7 +3419,7 @@ def concatenation_for_salutation(d):
|
||||
salutation = pgettext("person_name_salutation", salutation)
|
||||
given_name = None
|
||||
|
||||
return " ".join(str(p) for p in filter(None, (salutation, title, given_name, family_name)))
|
||||
return " ".join(filter(None, (salutation, title, given_name, family_name)))
|
||||
|
||||
|
||||
def get_name_parts_localized(name_parts, key):
|
||||
|
||||
@@ -294,16 +294,13 @@ This signal is sent out when a notification is sent.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_sales_channel_types = django.dispatch.Signal()
|
||||
register_sales_channels = django.dispatch.Signal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
|
||||
instance of a subclass of ``pretix.base.channels.SalesChannel`` or a list of such
|
||||
instances.
|
||||
"""
|
||||
|
||||
|
||||
register_sales_channels = DeprecatedSignal() # TODO: remove me
|
||||
|
||||
register_data_exporters = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to get all known data exporters. Receivers should return a
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user