mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac3230930 |
@@ -1,2 +0,0 @@
|
|||||||
# Format pre-vue code with eslint where possible (2026-09-10)
|
|
||||||
d85e52c83ed3639e040372fd0052e842e4899d90
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
name: SBOM
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master, sbom ]
|
|
||||||
tags: [ 'v.*' ]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
name: Submission
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '24.x'
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y gettext unzip
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: pip3 install -U "prisma-sbom-submit[python]"
|
|
||||||
- name: Create SBOM
|
|
||||||
run: NPM=$(which npm) prisma-sbom-submit collect . sbom.json
|
|
||||||
- name: Submit SBOM
|
|
||||||
run: prisma-sbom-submit upload --server https://prisma.pretix.com sbom.json
|
|
||||||
env:
|
|
||||||
PRISMA_UPLOAD_TOKEN: ${{ secrets.PRISMA_UPLOAD_TOKEN }}
|
|
||||||
@@ -123,24 +123,7 @@ jobs:
|
|||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: make all compress
|
run: make all compress
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
run: playwright install --with-deps
|
run: playwright install
|
||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 --tracing=retain-on-failure
|
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
with:
|
|
||||||
name: playwright-traces
|
|
||||||
path: test-results/
|
|
||||||
- name: Log trace instructions
|
|
||||||
if: steps.check-traces.outputs.found == 'true'
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "## 🎭 Playwright traces available"
|
|
||||||
echo ""
|
|
||||||
echo "Some tests failed or retried and produced traces."
|
|
||||||
echo ""
|
|
||||||
echo "1. Download the **playwright-traces-${{ github.run_id }}** artifact from this run (link in the **Summary** tab, under Artifacts)."
|
|
||||||
echo "2. Unzip it."
|
|
||||||
echo "3. Go to https://trace.playwright.dev and drag \`trace.zip\` into the page — or run \`npx playwright show-trace trace.zip\` locally."
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
|
- playwright install
|
||||||
|
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- '/^v.*$/'
|
- '/^v.*$/'
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
+2
-1
@@ -57,7 +57,8 @@ COPY vite.config.ts /pretix/vite.config.ts
|
|||||||
|
|
||||||
RUN pip3 install -U \
|
RUN pip3 install -U \
|
||||||
pip \
|
pip \
|
||||||
setuptools && \
|
setuptools \
|
||||||
|
wheel && \
|
||||||
cd /pretix && \
|
cd /pretix && \
|
||||||
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
||||||
-e ".[memcached]" \
|
-e ".[memcached]" \
|
||||||
|
|||||||
@@ -46,14 +46,12 @@ Checking a ticket in
|
|||||||
this request twice with the same nonce, the second request will also succeed but will always
|
this request twice with the same nonce, the second request will also succeed but will always
|
||||||
create only one check-in object even when the previous request was successful as well. This
|
create only one check-in object even when the previous request was successful as well. This
|
||||||
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
||||||
:<json string exchange_medium_type: To perform an exchange to a reusable medium, pass the type of the new reusable medium
|
|
||||||
:<json string exchange_medium_identifier: To perform an exchange to a reusable media, pass the identifier of the new medium
|
|
||||||
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
|
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
|
||||||
order) when building texts (currently only the ``reason_explanation`` response field).
|
order) when building texts (currently only the ``reason_explanation`` response field).
|
||||||
Defaults to ``false`` in which case the server will determine the language (currently
|
Defaults to ``false`` in which case the server will determine the language (currently
|
||||||
the event default language, might change in the future with support for the
|
the event default language, might change in the future with support for the
|
||||||
``Accept-Language`` header).
|
``Accept-Language`` header).
|
||||||
:>json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
|
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
|
||||||
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
||||||
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
||||||
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
|
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
|
||||||
@@ -69,10 +67,6 @@ Checking a ticket in
|
|||||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||||
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
||||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||||
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
|
||||||
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
|
||||||
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
|
|
||||||
object will not reflect the simulated changes.
|
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -230,9 +224,6 @@ Checking a ticket in
|
|||||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||||
* ``revoked`` - Ticket code has been revoked.
|
* ``revoked`` - Ticket code has been revoked.
|
||||||
* ``unapproved`` - Order has not yet been approved.
|
* ``unapproved`` - Order has not yet been approved.
|
||||||
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
|
|
||||||
* ``medium_invalid`` - Reusable medium identifier given was not found or is not valid.
|
|
||||||
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
|
|
||||||
* ``error`` - Internal error.
|
* ``error`` - Internal error.
|
||||||
|
|
||||||
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||||
|
|||||||
@@ -351,8 +351,7 @@ Endpoints
|
|||||||
|
|
||||||
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
|
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
|
||||||
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
|
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
|
||||||
:<json raw_barcode: The raw barcode or identifier you scanned. Required.
|
:<json raw_barcode: The raw barcode you scanned. Required.
|
||||||
:<json raw_source_type: The type of medium you scanned, defaults to ``barcode``. Optional.
|
|
||||||
:<json datetime: Date and time of the scan. Optional.
|
:<json datetime: Date and time of the scan. Optional.
|
||||||
:<json type: Type of scan, defaults to ``"entry"``.
|
:<json type: Type of scan, defaults to ``"entry"``.
|
||||||
:<json position: Internal ID of an order position you matched. Optional.
|
:<json position: Internal ID of an order position you matched. Optional.
|
||||||
@@ -603,8 +602,7 @@ Order position endpoints
|
|||||||
|
|
||||||
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
|
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
|
||||||
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
|
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
|
||||||
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead. Advanced features like medium
|
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead.
|
||||||
exchange are only supported on the new API.
|
|
||||||
|
|
||||||
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||||
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||||
@@ -743,9 +741,6 @@ Order position endpoints
|
|||||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||||
* ``revoked`` - Ticket code has been revoked.
|
* ``revoked`` - Ticket code has been revoked.
|
||||||
* ``unapproved`` - Order has not yet been approved.
|
* ``unapproved`` - Order has not yet been approved.
|
||||||
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
|
|
||||||
* ``medium_invalid`` - Reusable medium identifier given was not found and could not be automatically created.
|
|
||||||
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
|
|
||||||
|
|
||||||
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||||
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ organizer level.
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"region": "DE",
|
"imprint_url": "https://pretix.eu",
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,14 +579,12 @@ organizer level.
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"region":
|
"imprint_url":
|
||||||
{
|
{
|
||||||
"value": "DE",
|
"value": "https://pretix.eu",
|
||||||
"label": "Region",
|
"label": "Imprint URL",
|
||||||
"readonly": false,
|
"readonly": false,
|
||||||
"help_text": "Will be used to determine date and time formatting as well as default country for customer
|
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||||
addresses and phone numbers. For formatting, this takes less priority than the language and
|
|
||||||
is therefore mostly relevant for languages used in different regions globally (like English)."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
…
|
…
|
||||||
@@ -622,7 +620,7 @@ organizer level.
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"region": "DE"
|
"imprint_url": "https://example.org/imprint/"
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -634,7 +632,7 @@ organizer level.
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"region": "DE",
|
"imprint_url": "https://example.org/imprint/",
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ allow_waitinglist boolean If ``false``,
|
|||||||
product when it is sold out.
|
product when it is sold out.
|
||||||
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
||||||
media_policy string Policy on how to handle reusable media (experimental feature).
|
media_policy string Policy on how to handle reusable media (experimental feature).
|
||||||
Possible values are ``null``, ``"new"``, ``"reuse"``, ``"reuse_or_new"``, ``"append"``, and ``"append_or_new"``.
|
Possible values are ``null``, ``"new"``, ``"reuse"``, and ``"reuse_or_new"``.
|
||||||
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
|
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
|
||||||
show_quota_left boolean Publicly show how many tickets are still available.
|
show_quota_left boolean Publicly show how many tickets are still available.
|
||||||
If this is ``null``, the event default is used.
|
If this is ``null``, the event default is used.
|
||||||
|
|||||||
@@ -864,9 +864,6 @@ Generating new secrets
|
|||||||
|
|
||||||
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
|
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
Ticket secrets of order positions that have been used to issue a gift card can not
|
|
||||||
be changed. Only the link (``web_secret``) will be changed in this case.
|
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -898,9 +895,6 @@ Generating new secrets
|
|||||||
|
|
||||||
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||||
|
|
||||||
Ticket secrets of order positions that have been used to issue a gift card can not
|
|
||||||
be changed. Only the link (``web_secret``) will be changed in this case.
|
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1075,7 +1069,8 @@ Creating orders
|
|||||||
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
||||||
* ``use_reusable_medium`` (optional, causes the new ticket to be connected to the given reusable medium, identified by its ID)
|
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
||||||
|
* ``add_to_reusable_medium`` (optional, causes the new ticket to be added to the given reusable medium, identified by its ID)
|
||||||
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
@@ -2038,7 +2033,7 @@ Manipulating individual positions
|
|||||||
|
|
||||||
* ``order`` (mandatory, specified as a string mapping to a ``code``)
|
* ``order`` (mandatory, specified as a string mapping to a ``code``)
|
||||||
|
|
||||||
* ``addon_to`` (optional, specified as an integer mapping to ``positionid`` - the number of the position within the order, see :ref:`_order-position-resource` - of the parent position)
|
* ``addon_to`` (optional, specified as an integer mapping to the ``positionid`` of the parent position)
|
||||||
|
|
||||||
* ``item`` (mandatory)
|
* ``item`` (mandatory)
|
||||||
|
|
||||||
@@ -2348,7 +2343,7 @@ otherwise, such as splitting an order or changing fees.
|
|||||||
"subevent": 562,
|
"subevent": 562,
|
||||||
"seat": "seat-guid-2",
|
"seat": "seat-guid-2",
|
||||||
"price": "99.99",
|
"price": "99.99",
|
||||||
"addon_to": 1,
|
"addon_to": 12374,
|
||||||
"attendee_name": "Peter",
|
"attendee_name": "Peter",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ valid_date_max date Maximum value f
|
|||||||
valid_datetime_min datetime Minimum value for date and time questions (optional)
|
valid_datetime_min datetime Minimum value for date and time questions (optional)
|
||||||
valid_datetime_max datetime Maximum value for date and time questions (optional)
|
valid_datetime_max datetime Maximum value for date and time questions (optional)
|
||||||
valid_file_portrait boolean Turn on file validation for portrait photos
|
valid_file_portrait boolean Turn on file validation for portrait photos
|
||||||
valid_string_length_min integer Minimum length for string questions (optional)
|
|
||||||
valid_string_length_max integer Maximum length for string questions (optional)
|
valid_string_length_max integer Maximum length for string questions (optional)
|
||||||
dependency_question integer Internal ID of a different question. The current
|
dependency_question integer Internal ID of a different question. The current
|
||||||
question will only be shown if the question given in
|
question will only be shown if the question given in
|
||||||
@@ -131,7 +130,6 @@ Endpoints
|
|||||||
"valid_date_max": null,
|
"valid_date_max": null,
|
||||||
"valid_datetime_min": null,
|
"valid_datetime_min": null,
|
||||||
"valid_datetime_max": null,
|
"valid_datetime_max": null,
|
||||||
"valid_string_length_min": null,
|
|
||||||
"valid_string_length_max": null,
|
"valid_string_length_max": null,
|
||||||
"valid_file_portrait": false,
|
"valid_file_portrait": false,
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
@@ -213,7 +211,6 @@ Endpoints
|
|||||||
"valid_datetime_min": null,
|
"valid_datetime_min": null,
|
||||||
"valid_datetime_max": null,
|
"valid_datetime_max": null,
|
||||||
"valid_file_portrait": false,
|
"valid_file_portrait": false,
|
||||||
"valid_string_length_min": null,
|
|
||||||
"valid_string_length_max": null,
|
"valid_string_length_max": null,
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_value": null,
|
"dependency_value": null,
|
||||||
@@ -318,7 +315,6 @@ Endpoints
|
|||||||
"valid_datetime_min": null,
|
"valid_datetime_min": null,
|
||||||
"valid_datetime_max": null,
|
"valid_datetime_max": null,
|
||||||
"valid_file_portrait": false,
|
"valid_file_portrait": false,
|
||||||
"valid_string_length_min": null,
|
|
||||||
"valid_string_length_max": null,
|
"valid_string_length_max": null,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
@@ -403,7 +399,6 @@ Endpoints
|
|||||||
"valid_datetime_min": null,
|
"valid_datetime_min": null,
|
||||||
"valid_datetime_max": null,
|
"valid_datetime_max": null,
|
||||||
"valid_file_portrait": false,
|
"valid_file_portrait": false,
|
||||||
"valid_string_length_min": null,
|
|
||||||
"valid_string_length_max": null,
|
"valid_string_length_max": null,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ is a python method that emulates a behavior similar to ``reverse``:
|
|||||||
|
|
||||||
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
|
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
|
||||||
|
|
||||||
.. autofunction:: pretix.multidomain.urlreverse.eventreverse_absolute
|
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
|
||||||
|
|
||||||
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
|
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
|
||||||
as its first argument and can be used like this::
|
as its first argument and can be used like this::
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Working with the code
|
|||||||
---------------------
|
---------------------
|
||||||
If you do not have a recent installation of ``nodejs``, install it now::
|
If you do not have a recent installation of ``nodejs``, install it now::
|
||||||
|
|
||||||
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
||||||
sudo apt install nodejs
|
sudo apt install nodejs
|
||||||
|
|
||||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||||
|
|||||||
+1
-58
@@ -8,64 +8,7 @@ import vuePug from 'eslint-plugin-vue-pug'
|
|||||||
|
|
||||||
const ignores = globalIgnores([
|
const ignores = globalIgnores([
|
||||||
'**/node_modules',
|
'**/node_modules',
|
||||||
'**/dist',
|
'**/dist'
|
||||||
// Vendored code
|
|
||||||
'src/pretix/static/leaflet',
|
|
||||||
'src/pretix/static/clipboard',
|
|
||||||
'src/pretix/static/cropper',
|
|
||||||
'src/pretix/static/lightbox',
|
|
||||||
'src/pretix/static/are-you-sure',
|
|
||||||
'src/pretix/static/vuejs',
|
|
||||||
'src/pretix/static/fontawesome',
|
|
||||||
'src/pretix/static/typeahead',
|
|
||||||
'src/pretix/static/moment',
|
|
||||||
'src/pretix/static/pdfjs',
|
|
||||||
'src/pretix/static/sortable',
|
|
||||||
'src/pretix/static/iframeresizer',
|
|
||||||
'src/pretix/static/bootstrap',
|
|
||||||
'src/pretix/static/d3',
|
|
||||||
'src/pretix/static/jsi18n',
|
|
||||||
'src/pretix/static/fabric',
|
|
||||||
'src/pretix/static/datetimepicker',
|
|
||||||
'src/pretix/static/charts',
|
|
||||||
'src/pretix/static/fileupload',
|
|
||||||
'src/pretix/static/seating',
|
|
||||||
'src/pretix/static/rest_framework',
|
|
||||||
'src/pretix/static/select2',
|
|
||||||
'src/pretix/static/schema',
|
|
||||||
'src/pretix/static/slider',
|
|
||||||
'src/pretix/static/jquery',
|
|
||||||
'src/pretix/static/colorpicker',
|
|
||||||
'src/pretix/static/rrule',
|
|
||||||
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/widget/docready.js',
|
|
||||||
// Pre-vue JS code
|
|
||||||
'src/pretix/static/pretixbase/js/addressform.js',
|
|
||||||
'src/pretix/static/pretixbase/js/asynctask.js',
|
|
||||||
'src/pretix/static/pretixbase/js/details.js',
|
|
||||||
'src/pretix/static/pretixbase/js/gettextstub.js',
|
|
||||||
'src/pretix/static/pretixbase/js/i18nstring.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/menu.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/editor.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/geo.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/main.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/variations.js',
|
|
||||||
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/ui/cart.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/ui/main.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/ui/questions.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
|
|
||||||
'src/pretix/static/pretixpresale/js/widget/widget.js',
|
|
||||||
'src/pretix/plugins/banktransfer/static',
|
|
||||||
'src/pretix/plugins/paypal2/static',
|
|
||||||
'src/pretix/plugins/statistics/static',
|
|
||||||
'src/pretix/plugins/stripe/static',
|
|
||||||
// Plugin checkouts
|
|
||||||
'local',
|
|
||||||
// docs
|
|
||||||
'doc',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
|
|||||||
Generated
+98
-126
@@ -9,8 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.30",
|
"vue": "^3.5.30"
|
||||||
"vue-slicksort": "^2.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -371,14 +370,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.2"
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -428,9 +427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.133.0",
|
"version": "0.129.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -759,9 +758,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -776,9 +775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -793,9 +792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -810,9 +809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -827,9 +826,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
|
||||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -844,16 +843,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -864,16 +860,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -884,16 +877,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -904,16 +894,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -924,16 +911,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -944,16 +928,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -964,9 +945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -981,9 +962,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
|
||||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -1000,9 +981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1017,9 +998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2585,9 +2566,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.9",
|
"version": "5.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3177,9 +3158,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.16",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3353,9 +3334,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.23",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3372,7 +3353,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.16",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3629,14 +3610,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.129.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3645,27 +3626,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.3",
|
"@rolldown/binding-android-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
"@rolldown/binding-darwin-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
"@rolldown/binding-darwin-x64": "1.0.0",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
"@rolldown/binding-freebsd-x64": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
"@rolldown/binding-linux-x64-musl": "1.0.0",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
"@rolldown/binding-openharmony-arm64": "1.0.0",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
"@rolldown/binding-wasm32-wasi": "1.0.0",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -4149,9 +4130,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/smol-toml": {
|
"node_modules/smol-toml": {
|
||||||
"version": "1.7.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||||
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
|
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4344,9 +4325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4484,17 +4465,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.16",
|
"version": "8.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.14",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "1.0.0",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -4636,15 +4617,6 @@
|
|||||||
"vue-eslint-parser": "^10.0.0"
|
"vue-eslint-parser": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-slicksort": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-slicksort/-/vue-slicksort-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-fXz1YrNjhUbJK7o0tMk27mIr4pMAZYLSYvtmLazCtfpvz+zafPCn34ILDL8B7hT7WLVZKreYs6JVe5VWymqmzA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": ">=3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
+1
-2
@@ -27,8 +27,7 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.30",
|
"vue": "^3.5.30"
|
||||||
"vue-slicksort": "^2.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
+27
-26
@@ -29,34 +29,33 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.15.*",
|
"BeautifulSoup4==4.14.*",
|
||||||
"bleach==6.4.*",
|
"bleach==6.4.*",
|
||||||
"celery==5.6.*",
|
"celery==5.6.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=50.0.0",
|
"cryptography>=48.0.0",
|
||||||
"css-inline==0.21.*",
|
"css-inline==0.20.*",
|
||||||
"defusedcsv>=3.0.0",
|
"defusedcsv>=3.0.0",
|
||||||
"dnspython==2.*",
|
"dnspython==2.*",
|
||||||
"Django[argon2]==5.2.*,>=5.2.17",
|
"Django[argon2]==5.2.*",
|
||||||
"django-bootstrap3==26.2",
|
"django-bootstrap3==26.1",
|
||||||
"django-compressor==4.6.0",
|
"django-compressor==4.6.0",
|
||||||
"django-countries==9.0.*",
|
"django-countries==8.2.*",
|
||||||
"django-filter==26.1",
|
"django-filter==25.1",
|
||||||
"django-formset-js-improved==0.5.0.5",
|
"django-formset-js-improved==0.5.0.5",
|
||||||
"django-formtools==2.7",
|
"django-formtools==2.6.1",
|
||||||
"django-hierarkey==2.0.*,>=2.0.2",
|
"django-hierarkey==2.0.*,>=2.0.1",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.11.*",
|
"django-i18nfield==1.11.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==5.1",
|
"django-localflavor==5.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
"django-oauth-toolkit==2.3.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.7.*",
|
"django-otp==1.7.*",
|
||||||
"django-phonenumber-field==8.5.*",
|
"django-phonenumber-field==8.4.*",
|
||||||
"django-querytagger==0.0.3",
|
"django-redis==6.0.*",
|
||||||
"django-redis==7.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-scopes==2.1.*",
|
"django-statici18n==2.7.*",
|
||||||
"django-statici18n==2.8.*",
|
|
||||||
"djangorestframework==3.17.*",
|
"djangorestframework==3.17.*",
|
||||||
"dnspython==2.8.*",
|
"dnspython==2.8.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
@@ -67,7 +66,7 @@ dependencies = [
|
|||||||
"kombu==5.6.*",
|
"kombu==5.6.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.10.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.3.*",
|
"oauthlib==3.3.*",
|
||||||
@@ -77,14 +76,14 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.13.*",
|
"PyJWT==2.13.*",
|
||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==12.3.*",
|
"Pillow==12.2.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==7.36.*",
|
"protobuf==7.35.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==3.0",
|
"pycparser==3.0",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
"pypdf==6.11.*",
|
"pypdf==6.5.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
@@ -92,17 +91,17 @@ dependencies = [
|
|||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==7.4.*",
|
"redis==7.4.*",
|
||||||
"reportlab==5.0.*",
|
"reportlab==4.5.*",
|
||||||
"requests==2.34.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.68.*",
|
"sentry-sdk==2.61.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2026072401",
|
"tlds>=2026041800",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==3.0.*",
|
"webauthn==2.7.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ dev = [
|
|||||||
"aiohttp==3.14.*",
|
"aiohttp==3.14.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.37.*",
|
"fakeredis==2.36.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==8.0.*",
|
"isort==8.0.*",
|
||||||
@@ -126,7 +125,7 @@ dev = [
|
|||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.8.*",
|
||||||
"pytest-playwright",
|
"pytest-playwright",
|
||||||
"pytest==9.1.*",
|
"pytest==9.0.*",
|
||||||
"playwright",
|
"playwright",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
@@ -140,6 +139,8 @@ build-backend = "backend"
|
|||||||
backend-path = ["_build"]
|
backend-path = ["_build"]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
|
"setuptools-rust",
|
||||||
|
"wheel",
|
||||||
"importlib_metadata",
|
"importlib_metadata",
|
||||||
"tomli",
|
"tomli",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ localecompile:
|
|||||||
./manage.py compilemessages
|
./manage.py compilemessages
|
||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
|
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||||
./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||||
|
|
||||||
staticfiles: npminstall npmbuild jsi18n
|
staticfiles: npminstall npmbuild jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.8.0.dev0"
|
__version__ = "2026.6.0.dev0"
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ ALL_LANGUAGES = [
|
|||||||
('gl', _('Galician')),
|
('gl', _('Galician')),
|
||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
('he', _('Hebrew')),
|
('he', _('Hebrew')),
|
||||||
('hu', _('Hungarian')),
|
|
||||||
('id', _('Indonesian')),
|
('id', _('Indonesian')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
('ja', _('Japanese')),
|
('ja', _('Japanese')),
|
||||||
@@ -119,7 +118,6 @@ ALL_LANGUAGES = [
|
|||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('es-419', _('Spanish (Latin America)')),
|
('es-419', _('Spanish (Latin America)')),
|
||||||
('th', _('Thai')),
|
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,11 +20,8 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.db import DatabaseError
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
@@ -33,7 +30,6 @@ from pretix.api.auth.devicesecurity import (
|
|||||||
FullAccessSecurityProfile, get_all_security_profiles,
|
FullAccessSecurityProfile, get_all_security_profiles,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
from pretix.base.models.devices import DeviceLastSeen
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,7 +42,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
model = self.get_model()
|
model = self.get_model()
|
||||||
try:
|
try:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
device = model.objects.select_related('organizer', 'last_seen').get(api_token=key)
|
device = model.objects.select_related('organizer').get(api_token=key)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||||
|
|
||||||
@@ -57,7 +53,6 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
logging.warning(f'Connection attempt of revoked device {device.pk}.')
|
logging.warning(f'Connection attempt of revoked device {device.pk}.')
|
||||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||||
|
|
||||||
self._update_last_seen(device)
|
|
||||||
return AnonymousUser(), device
|
return AnonymousUser(), device
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
@@ -68,22 +63,3 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
if not profile.is_allowed(request):
|
if not profile.is_allowed(request):
|
||||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def _update_last_seen(self, device: Device):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
last_seen_obj = device.last_seen
|
|
||||||
except DeviceLastSeen.DoesNotExist:
|
|
||||||
# First request from device, create model, ignore result. Use get_or_create to be safe
|
|
||||||
# against concurrent create requests
|
|
||||||
DeviceLastSeen.objects.get_or_create(device=device, last_seen=now())
|
|
||||||
else:
|
|
||||||
if now() - last_seen_obj.last_seen < timedelta(seconds=10):
|
|
||||||
# We don't need to know the last seen info of a device to more precision than this,
|
|
||||||
# so we can avoid some database writes if the device is bursting a lot of requests.
|
|
||||||
return
|
|
||||||
last_seen_obj.last_seen = now()
|
|
||||||
last_seen_obj.save(update_fields=["last_seen"])
|
|
||||||
except DatabaseError:
|
|
||||||
# Do not stop the request from happening
|
|
||||||
logger.exception("Database error while updating last_seen")
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -51,18 +52,10 @@ class BaseSecurityProfile:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@property
|
|
||||||
def priority(self) -> int:
|
|
||||||
"""
|
|
||||||
Priority for ordering, higher will come first.
|
|
||||||
"""
|
|
||||||
return 100
|
|
||||||
|
|
||||||
|
|
||||||
class FullAccessSecurityProfile(BaseSecurityProfile):
|
class FullAccessSecurityProfile(BaseSecurityProfile):
|
||||||
identifier = 'full'
|
identifier = 'full'
|
||||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||||
priority = 1000
|
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
return True
|
return True
|
||||||
@@ -115,11 +108,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
('POST', 'api-v1:checkinrpc.annul'),
|
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
('GET', 'api-v1:reusablemedium-list'),
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
|
||||||
('PATCH', 'api-v1:reusablemedium-detail')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -154,7 +144,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
('POST', 'api-v1:checkinrpc.annul'),
|
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -191,7 +180,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
('POST', 'api-v1:checkinrpc.annul'),
|
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,15 +190,13 @@ def get_all_security_profiles():
|
|||||||
if _ALL_PROFILES:
|
if _ALL_PROFILES:
|
||||||
return _ALL_PROFILES
|
return _ALL_PROFILES
|
||||||
|
|
||||||
types = []
|
types = OrderedDict()
|
||||||
for recv, ret in register_device_security_profile.send(None):
|
for recv, ret in register_device_security_profile.send(None):
|
||||||
if isinstance(ret, (list, tuple)):
|
if isinstance(ret, (list, tuple)):
|
||||||
for r in ret:
|
for r in ret:
|
||||||
types.append(r)
|
types[r.identifier] = r
|
||||||
else:
|
else:
|
||||||
types.append(ret)
|
types[ret.identifier] = ret
|
||||||
types.sort(key=lambda el: el.priority, reverse=True)
|
|
||||||
types = {r.identifier: r for r in types}
|
|
||||||
_ALL_PROFILES = types
|
_ALL_PROFILES = types
|
||||||
return types
|
return types
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
|
|
||||||
from django.db.models import prefetch_related_objects
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@@ -136,30 +135,3 @@ class SalesChannelMigrationMixin:
|
|||||||
else:
|
else:
|
||||||
value["sales_channels"] = value["limit_sales_channels"]
|
value["sales_channels"] = value["limit_sales_channels"]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class CompatDecimalField(serializers.DecimalField):
|
|
||||||
"""
|
|
||||||
Historically, pretix recorded tax rates as decimals with two places. Today, pretix supports tax rates with up to
|
|
||||||
four places. Since our API outputs decimals with the stored precision, this would have changed the API output from
|
|
||||||
"19.00" to "19.0000" without warning. While this is semantically the same thing, we need to assume some pretix API
|
|
||||||
users might run into trouble, either because they treat the value as a string and then map something
|
|
||||||
(e.g. ``if tax_rate == "19.00"``) or process it with a language where this is a significant difference. For example,
|
|
||||||
while in Python ``Decimal("19.00") == Decimal("19.0000")`` is true, in Java
|
|
||||||
``(new BigDecimal("19.00")).equals(new BigDecimal("19.0000"))`` is false and only
|
|
||||||
``(new BigDecimal("19.00")).compareTo(new BigDecimal("19.0000")) == 0`` is true.
|
|
||||||
|
|
||||||
Therefore, we stay backwards compatible by outputting two decimal places *as long as the trailing digits are zero-valued.
|
|
||||||
"""
|
|
||||||
|
|
||||||
regex = re.compile(r"^([0-9]+\.[0-9]{2})0+$")
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
if self.localize:
|
|
||||||
raise ValueError("localization not supported")
|
|
||||||
value = super().to_representation(value)
|
|
||||||
if value and "." not in value:
|
|
||||||
return f"{value}.00"
|
|
||||||
if m := self.regex.match(value):
|
|
||||||
return m.group(1)
|
|
||||||
return value
|
|
||||||
|
|||||||
@@ -88,20 +88,11 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
|||||||
nonce = serializers.CharField(required=False, allow_null=True)
|
nonce = serializers.CharField(required=False, allow_null=True)
|
||||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
answers = serializers.JSONField(required=False, allow_null=True)
|
answers = serializers.JSONField(required=False, allow_null=True)
|
||||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
|
||||||
exchange_medium_identifier = serializers.CharField(required=False)
|
|
||||||
simulate = serializers.BooleanField(default=False, required=False)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
exchange_fields = ["exchange_medium_type", "exchange_medium_identifier"]
|
|
||||||
if any(attrs.get(k) is None for k in exchange_fields) and not all(attrs.get(k) is None for k in exchange_fields):
|
|
||||||
raise ValidationError("If you set any of exchange_medium_type or exchange_medium_identifier, you need to set both of them.")
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
||||||
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from rest_framework.fields import ChoiceField, Field
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatDecimalField, CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.fields import PluginsField
|
from pretix.api.serializers.fields import PluginsField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -73,7 +73,7 @@ from pretix.base.settings import (
|
|||||||
LazyI18nStringList, validate_event_settings,
|
LazyI18nStringList, validate_event_settings,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import api_event_settings_fields
|
from pretix.base.signals import api_event_settings_fields
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_event_url(self, event):
|
def get_event_url(self, event):
|
||||||
return eventreverse_absolute(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
@@ -681,7 +681,6 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
@@ -702,12 +701,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
return super().save(**kwargs)
|
return super().save(**kwargs)
|
||||||
|
|
||||||
def validate_default(self, value):
|
def validate_default(self, value):
|
||||||
if not value:
|
if not value and self.instance.default:
|
||||||
if self.instance:
|
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
||||||
if self.instance.default:
|
|
||||||
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
|
||||||
elif not self.context["event"].tax_rules.exists():
|
|
||||||
raise ValidationError("You can't remove the default property as there is only one tax rule.")
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -752,7 +747,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'show_variations_expanded',
|
'show_variations_expanded',
|
||||||
'hide_sold_out',
|
'hide_sold_out',
|
||||||
'meta_noindex',
|
'meta_noindex',
|
||||||
@@ -877,7 +871,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'og_image',
|
'og_image',
|
||||||
'name_scheme',
|
'name_scheme',
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
@@ -892,7 +885,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
# These are read-only since they are currently only settable on organizers, not events
|
# These are read-only since they are currently only settable on organizers, not events
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
@@ -978,8 +970,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
'reusable_media_usage_enforced',
|
'system_question_order',
|
||||||
'system_question_order', # TODO(questionnaires) - remove or replace
|
|
||||||
'tax_rule_payment',
|
'tax_rule_payment',
|
||||||
'tax_rule_cancellation',
|
'tax_rule_cancellation',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
CompatDecimalField, SalesChannelMigrationMixin,
|
|
||||||
)
|
|
||||||
from pretix.api.serializers.event import MetaDataField
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -53,7 +51,6 @@ from pretix.base.models import (
|
|||||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||||
SalesChannel,
|
SalesChannel,
|
||||||
)
|
)
|
||||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
|
||||||
|
|
||||||
|
|
||||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
@@ -279,10 +276,10 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ItemTaxRateField(CompatDecimalField):
|
class ItemTaxRateField(serializers.Field):
|
||||||
def to_representation(self, i):
|
def to_representation(self, i):
|
||||||
if i.tax_rule:
|
if i.tax_rule:
|
||||||
return super().to_representation(Decimal(i.tax_rule.rate))
|
return str(Decimal(i.tax_rule.rate))
|
||||||
else:
|
else:
|
||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
@@ -292,7 +289,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True, max_digits=7, decimal_places=4)
|
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
'image/png', 'image/jpeg', 'image/gif'
|
'image/png', 'image/jpeg', 'image/gif'
|
||||||
@@ -543,7 +540,6 @@ class LegacyDependencyValueField(serializers.CharField):
|
|||||||
class QuestionSerializer(I18nAwareModelSerializer):
|
class QuestionSerializer(I18nAwareModelSerializer):
|
||||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||||
identifier = serializers.CharField(allow_null=True)
|
identifier = serializers.CharField(allow_null=True)
|
||||||
internal_name = serializers.CharField(allow_null=True, source='question', read_only=True)
|
|
||||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -552,7 +548,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'internal_name',)
|
'valid_string_length_max', 'valid_file_portrait')
|
||||||
|
|
||||||
def validate_identifier(self, value):
|
def validate_identifier(self, value):
|
||||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||||
@@ -621,167 +617,13 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items', [])
|
items = validated_data.pop('items', [])
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data, container_type=Question.ContainerType.ORDERPOSITION)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
for opt_data in options_data:
|
for opt_data in options_data:
|
||||||
QuestionOption.objects.create(question=question, **opt_data)
|
QuestionOption.objects.create(question=question, **opt_data)
|
||||||
return question
|
return question
|
||||||
|
|
||||||
|
|
||||||
class QuestionRefField(serializers.PrimaryKeyRelatedField):
|
|
||||||
def to_representation(self, qc):
|
|
||||||
if not qc:
|
|
||||||
return None
|
|
||||||
elif qc.system_datafield:
|
|
||||||
return qc.system_datafield
|
|
||||||
elif qc.user_datafield_id:
|
|
||||||
return qc.user_datafield_id
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if type(data) == int:
|
|
||||||
return {'user_datafield': super().to_internal_value(data), 'system_datafield': None}
|
|
||||||
elif type(data) == str or data is None:
|
|
||||||
return {'user_datafield': None, 'system_datafield': data}
|
|
||||||
else:
|
|
||||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
|
||||||
|
|
||||||
def use_pk_only_optimization(self):
|
|
||||||
return self.source == '*'
|
|
||||||
|
|
||||||
|
|
||||||
class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
|
||||||
question = QuestionRefField(source='*', queryset=Question.objects.none())
|
|
||||||
dependency_question = QuestionRefField(allow_null=True, required=False, queryset=Question.objects.none())
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = QuestionnaireChild
|
|
||||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values')
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["question"].queryset = self.context["event"].questions.all()
|
|
||||||
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
event = self.context['event']
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
|
||||||
raise ValidationError('Dependencies are not supported during check-in.')
|
|
||||||
|
|
||||||
dep = full_data.get('dependency_question')
|
|
||||||
if dep:
|
|
||||||
if dep.ask_during_checkin:
|
|
||||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
|
||||||
|
|
||||||
seen_ids = {self.instance.pk} if self.instance else set()
|
|
||||||
while dep:
|
|
||||||
if dep.pk in seen_ids:
|
|
||||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
|
||||||
seen_ids.add(dep.pk)
|
|
||||||
dep = dep.dependency_question
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def validate_dependency_question(self, value):
|
|
||||||
if value:
|
|
||||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
|
||||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
|
||||||
if value == self.instance:
|
|
||||||
raise ValidationError('A question cannot depend on itself.')
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
|
||||||
limit_sales_channels = serializers.SlugRelatedField(
|
|
||||||
slug_field="identifier",
|
|
||||||
queryset=SalesChannel.objects.none(),
|
|
||||||
required=False,
|
|
||||||
allow_empty=True,
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Questionnaire
|
|
||||||
fields = ('id', 'type', 'internal_name', 'items', 'position', 'all_sales_channels', 'limit_sales_channels', 'children')
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
event = self.context['event']
|
|
||||||
|
|
||||||
#full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
#full_data.update(data)
|
|
||||||
|
|
||||||
#if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
|
||||||
# raise ValidationError('Dependencies are not supported during check-in.')
|
|
||||||
|
|
||||||
#if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
|
||||||
# raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
|
||||||
|
|
||||||
#if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
|
||||||
# raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
|
||||||
|
|
||||||
#Question.clean_items(event, full_data.get('items') or [])
|
|
||||||
return data
|
|
||||||
|
|
||||||
def validate_children(self, value):
|
|
||||||
prev_questions = {}
|
|
||||||
for child in value:
|
|
||||||
if child.get('dependency_question'):
|
|
||||||
if (child['dependency_question']['user_datafield'] or child['dependency_question']['system_datafield']) not in prev_questions:
|
|
||||||
raise ValidationError('A question can only depend on a previous question from the same questionnaire.')
|
|
||||||
|
|
||||||
if child['user_datafield']:
|
|
||||||
prev_questions[child['user_datafield']] = child
|
|
||||||
if child['system_datafield']:
|
|
||||||
prev_questions[child['system_datafield']] = child
|
|
||||||
return value
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
|
||||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
|
||||||
questionnaire = super().create(validated_data)
|
|
||||||
self.set_children(questionnaire, children_data)
|
|
||||||
return questionnaire
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
children_data = validated_data.pop('children', None)
|
|
||||||
questionnaire = super().update(instance, validated_data)
|
|
||||||
if children_data is not None:
|
|
||||||
self.set_children(questionnaire, children_data)
|
|
||||||
return questionnaire
|
|
||||||
|
|
||||||
def set_children(self, questionnaire, new_data):
|
|
||||||
result = []
|
|
||||||
child_serializer = self.fields['children'].child
|
|
||||||
existing = questionnaire.children.all()
|
|
||||||
for i, d in enumerate(new_data):
|
|
||||||
d['questionnaire'] = questionnaire
|
|
||||||
d['position'] = i + 1
|
|
||||||
d.setdefault('required', False)
|
|
||||||
d.setdefault('help_text', None)
|
|
||||||
d.setdefault('dependency_question', None)
|
|
||||||
d.setdefault('dependency_values', None)
|
|
||||||
updatable = min(len(existing), len(new_data))
|
|
||||||
for i in range(0, updatable):
|
|
||||||
result.append(child_serializer.update(existing[i], new_data[i]))
|
|
||||||
for i in range(updatable, len(new_data)):
|
|
||||||
result.append(child_serializer.create(new_data[i]))
|
|
||||||
for i in range(updatable, len(existing)):
|
|
||||||
existing[i].delete()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class QuotaSerializer(I18nAwareModelSerializer):
|
class QuotaSerializer(I18nAwareModelSerializer):
|
||||||
available = serializers.BooleanField(read_only=True)
|
available = serializers.BooleanField(read_only=True)
|
||||||
available_number = serializers.IntegerField(read_only=True)
|
available_number = serializers.IntegerField(read_only=True)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -52,7 +52,6 @@ from pretix.api.signals import order_api_details, orderposition_api_details
|
|||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoicing.transmission import get_transmission_types
|
from pretix.base.invoicing.transmission import get_transmission_types
|
||||||
from pretix.base.media import MEDIA_TYPES
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||||
@@ -77,7 +76,7 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -382,7 +381,6 @@ class PrintLogSerializer(serializers.ModelSerializer):
|
|||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||||
raw_source_type = serializers.ChoiceField(choices=[(k, v) for k, v in MEDIA_TYPES.items()], default='barcode')
|
|
||||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
|
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
|
||||||
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
||||||
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
||||||
@@ -392,7 +390,7 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
|
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
|
||||||
'raw_subevent', 'raw_source_type', 'nonce', 'datetime', 'type', 'position')
|
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -593,7 +591,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
attendee_name = serializers.CharField(required=False)
|
attendee_name = serializers.CharField(required=False)
|
||||||
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
||||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = OrderPositionListSerializer
|
list_serializer_class = OrderPositionListSerializer
|
||||||
@@ -750,8 +747,6 @@ class OrderPaymentDateField(serializers.DateField):
|
|||||||
|
|
||||||
|
|
||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderFee
|
model = OrderFee
|
||||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
||||||
@@ -762,7 +757,7 @@ class PaymentURLField(serializers.URLField):
|
|||||||
def to_representation(self, instance: OrderPayment):
|
def to_representation(self, instance: OrderPayment):
|
||||||
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||||
return None
|
return None
|
||||||
return eventreverse_absolute(instance.order.event, 'presale:event.order.pay', kwargs={
|
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={
|
||||||
'order': instance.order.code,
|
'order': instance.order.code,
|
||||||
'secret': instance.order.secret,
|
'secret': instance.order.secret,
|
||||||
'payment': instance.pk,
|
'payment': instance.pk,
|
||||||
@@ -811,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class OrderURLField(serializers.URLField):
|
class OrderURLField(serializers.URLField):
|
||||||
def to_representation(self, instance: Order):
|
def to_representation(self, instance: Order):
|
||||||
return eventreverse_absolute(instance.event, 'presale:event.order', kwargs={
|
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={
|
||||||
'order': instance.code,
|
'order': instance.code,
|
||||||
'secret': instance.secret,
|
'secret': instance.secret,
|
||||||
})
|
})
|
||||||
@@ -1048,13 +1043,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
|
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
|
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
|
||||||
required=False, allow_null=True)
|
required=False, allow_null=True)
|
||||||
|
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
|
||||||
|
required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
||||||
'requested_valid_from', 'use_reusable_medium', 'discount')
|
'requested_valid_from', 'use_reusable_medium', 'add_to_reusable_medium', 'discount')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1066,6 +1063,8 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
if 'use_reusable_medium' in self.fields:
|
if 'use_reusable_medium' in self.fields:
|
||||||
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
|
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
|
||||||
|
if 'add_to_reusable_medium' in self.fields:
|
||||||
|
self.fields['add_to_reusable_medium'].queryset = ReusableMedium.objects.all()
|
||||||
|
|
||||||
def validate_secret(self, secret):
|
def validate_secret(self, secret):
|
||||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
@@ -1081,6 +1080,9 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
def validate_add_to_reusable_medium(self, m):
|
||||||
|
return self.validate_use_reusable_medium(m)
|
||||||
|
|
||||||
def validate_item(self, item):
|
def validate_item(self, item):
|
||||||
if item.event != self.context['event']:
|
if item.event != self.context['event']:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -1155,6 +1157,12 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'use_reusable_medium' in data and 'add_to_reusable_medium' in data:
|
||||||
|
raise ValidationError({
|
||||||
|
'use_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
|
||||||
|
'add_to_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
|
||||||
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1594,7 +1602,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos_data['attendee_name_parts'] = {
|
pos_data['attendee_name_parts'] = {
|
||||||
'_legacy': attendee_name
|
'_legacy': attendee_name
|
||||||
}
|
}
|
||||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium')})
|
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')})
|
||||||
if simulate:
|
if simulate:
|
||||||
pos.order = order._wrapped
|
pos.order = order._wrapped
|
||||||
else:
|
else:
|
||||||
@@ -1668,6 +1676,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
answers_data = pos_data.pop('answers', [])
|
answers_data = pos_data.pop('answers', [])
|
||||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||||
|
add_to_reusable_medium = pos_data.pop('add_to_reusable_medium', None)
|
||||||
pos = pos_data['__instance']
|
pos = pos_data['__instance']
|
||||||
pos._calculate_tax(invoice_address=ia)
|
pos._calculate_tax(invoice_address=ia)
|
||||||
|
|
||||||
@@ -1709,17 +1718,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
answ.options.add(*options)
|
answ.options.add(*options)
|
||||||
|
|
||||||
if use_reusable_medium:
|
if use_reusable_medium:
|
||||||
if pos.item.media_policy not in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
|
||||||
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
|
use_reusable_medium.log_action(
|
||||||
use_reusable_medium.log_action(
|
'pretix.reusable_medium.linked_orderposition.removed',
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
data={
|
||||||
data={
|
'linked_orderposition': op_pk,
|
||||||
'linked_orderposition': op_pk,
|
}
|
||||||
}
|
)
|
||||||
)
|
use_reusable_medium.linked_orderpositions.set([pos])
|
||||||
use_reusable_medium.linked_orderpositions.set([pos])
|
|
||||||
else:
|
|
||||||
use_reusable_medium.linked_orderpositions.add(pos)
|
|
||||||
use_reusable_medium.log_action(
|
use_reusable_medium.log_action(
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
'pretix.reusable_medium.linked_orderposition.added',
|
||||||
data={
|
data={
|
||||||
@@ -1727,7 +1733,15 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
'linked_orderposition': pos.pk,
|
'linked_orderposition': pos.pk,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
use_reusable_medium.touch()
|
elif add_to_reusable_medium:
|
||||||
|
add_to_reusable_medium.linked_orderpositions.add(pos)
|
||||||
|
add_to_reusable_medium.log_action(
|
||||||
|
'pretix.reusable_medium.linked_orderposition.added',
|
||||||
|
data={
|
||||||
|
'by_order': order.code,
|
||||||
|
'linked_orderposition': pos.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if not simulate:
|
if not simulate:
|
||||||
for cp in delete_cps:
|
for cp in delete_cps:
|
||||||
@@ -1916,7 +1930,6 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|||||||
position = LinePositionField(read_only=True)
|
position = LinePositionField(read_only=True)
|
||||||
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
||||||
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
||||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
@@ -2000,7 +2013,6 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class TransactionSerializer(I18nAwareModelSerializer):
|
class TransactionSerializer(I18nAwareModelSerializer):
|
||||||
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
||||||
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Transaction
|
model = Transaction
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ from pretix.helpers.permission_migration import (
|
|||||||
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
||||||
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
||||||
)
|
)
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
|
|||||||
slug = serializers.CharField(read_only=True)
|
slug = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return eventreverse_absolute(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
@@ -426,8 +426,6 @@ class TeamSerializer(serializers.ModelSerializer):
|
|||||||
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
||||||
if full_data.get(k) is True:
|
if full_data.get(k) is True:
|
||||||
data["limit_organizer_permissions"].update({kk: True for kk in v})
|
data["limit_organizer_permissions"].update({kk: True for kk in v})
|
||||||
for key in list(k for k in data if k.startswith("can_")):
|
|
||||||
del data[key]
|
|
||||||
|
|
||||||
if full_data.get('limit_events') and full_data.get('all_events'):
|
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||||
raise ValidationError('Do not set both limit_events and all_events.')
|
raise ValidationError('Do not set both limit_events and all_events.')
|
||||||
@@ -494,16 +492,14 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
|||||||
def _send_invite(self, instance):
|
def _send_invite(self, instance):
|
||||||
mail(
|
mail(
|
||||||
instance.email,
|
instance.email,
|
||||||
gettext('You\'ve been invited to join %(organizer)s') % {
|
_('Account invitation'),
|
||||||
'organizer': self.context['organizer'].name,
|
|
||||||
},
|
|
||||||
'pretixcontrol/email/invitation.txt',
|
'pretixcontrol/email/invitation.txt',
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.context['organizer'].name,
|
'organizer': self.context['organizer'].name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -578,7 +574,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'customer_accounts_require_login_for_order_access',
|
'customer_accounts_require_login_for_order_access',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
'organizer_info_text',
|
'organizer_info_text',
|
||||||
'event_list_type',
|
'event_list_type',
|
||||||
@@ -610,7 +605,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'cookie_consent_dialog_button_yes',
|
'cookie_consent_dialog_button_yes',
|
||||||
'cookie_consent_dialog_button_no',
|
'cookie_consent_dialog_button_no',
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ event_router.register(r'subevents', event.SubEventViewSet)
|
|||||||
event_router.register(r'clone', event.CloneEventViewSet)
|
event_router.register(r'clone', event.CloneEventViewSet)
|
||||||
event_router.register(r'items', item.ItemViewSet)
|
event_router.register(r'items', item.ItemViewSet)
|
||||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||||
event_router.register(r'datafields', item.QuestionViewSet)
|
event_router.register(r'questions', item.QuestionViewSet)
|
||||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
|
||||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||||
event_router.register(r'quotas', item.QuotaViewSet)
|
event_router.register(r'quotas', item.QuotaViewSet)
|
||||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||||
|
|||||||
@@ -69,10 +69,8 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.orders import PrintLog
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.permissions import AnyPermissionOf
|
from pretix.base.permissions import AnyPermissionOf
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
perform_checkin,
|
|
||||||
)
|
)
|
||||||
from pretix.base.services.media import perform_media_exchange
|
|
||||||
from pretix.base.signals import checkin_annulled
|
from pretix.base.signals import checkin_annulled
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
@@ -139,7 +137,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -154,7 +151,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -458,8 +454,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
|
|
||||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||||
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False,
|
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False):
|
||||||
exchange_medium_type=None, exchange_medium_identifier=None):
|
|
||||||
if not checkinlists:
|
if not checkinlists:
|
||||||
raise ValidationError('No check-in list passed.')
|
raise ValidationError('No check-in list passed.')
|
||||||
|
|
||||||
@@ -468,7 +463,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
|
|
||||||
device = auth if isinstance(auth, Device) else None
|
device = auth if isinstance(auth, Device) else None
|
||||||
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
||||||
medium = None
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
@@ -528,7 +522,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
# with respecting the force option), or it's a reusable medium (-> proceed with that)
|
# with respecting the force option), or it's a reusable medium (-> proceed with that)
|
||||||
if not op_candidates:
|
if not op_candidates:
|
||||||
try:
|
try:
|
||||||
medium = ReusableMedium.objects.active().filter(
|
media = ReusableMedium.objects.active().filter(
|
||||||
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
|
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
|
||||||
).get(
|
).get(
|
||||||
organizer_id=checkinlists[0].event.organizer_id,
|
organizer_id=checkinlists[0].event.organizer_id,
|
||||||
@@ -636,7 +630,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
else:
|
else:
|
||||||
linked_ops = medium.linked_orderpositions.all().select_related("order").prefetch_related("addons")
|
linked_ops = media.linked_orderpositions.all().select_related("order").prefetch_related("addons")
|
||||||
linked_event_ids = {op.order.event_id for op in linked_ops}
|
linked_event_ids = {op.order.event_id for op in linked_ops}
|
||||||
if not any(event_id in list_by_event for event_id in linked_event_ids):
|
if not any(event_id in list_by_event for event_id in linked_event_ids):
|
||||||
# Medium exists but connected ticket is for the wrong event
|
# Medium exists but connected ticket is for the wrong event
|
||||||
@@ -667,7 +661,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
op_candidates = []
|
op_candidates = []
|
||||||
for op in linked_ops:
|
for op in linked_ops:
|
||||||
if op.order.event_id in list_by_event:
|
if op.order.event_id in list_by_event:
|
||||||
reusable_medium_used = medium
|
reusable_medium_used = media
|
||||||
op_candidates.append(op)
|
op_candidates.append(op)
|
||||||
if list_by_event[op.order.event_id].addon_match:
|
if list_by_event[op.order.event_id].addon_match:
|
||||||
op_candidates += list(op.addons.all())
|
op_candidates += list(op.addons.all())
|
||||||
@@ -810,14 +804,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
locale = op.order.event.settings.locale
|
locale = op.order.event.settings.locale
|
||||||
with language(locale):
|
with language(locale):
|
||||||
try:
|
try:
|
||||||
if exchange_medium_identifier and medium:
|
perform_checkin(
|
||||||
# Cannot scan a medium and then request to exchange it
|
|
||||||
raise CheckInError(
|
|
||||||
gettext('You cannot exchange a medium for a medium.'),
|
|
||||||
'error'
|
|
||||||
)
|
|
||||||
|
|
||||||
checkin_args = dict(
|
|
||||||
op=op,
|
op=op,
|
||||||
clist=list_by_event[op.order.event_id],
|
clist=list_by_event[op.order.event_id],
|
||||||
given_answers=given_answers,
|
given_answers=given_answers,
|
||||||
@@ -835,30 +822,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
from_revoked_secret=from_revoked_secret,
|
from_revoked_secret=from_revoked_secret,
|
||||||
simulate=simulate,
|
simulate=simulate,
|
||||||
gate=gate,
|
gate=gate,
|
||||||
reusable_medium=medium,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
|
||||||
if simulate:
|
|
||||||
raise CheckInError(
|
|
||||||
gettext('You cannot simulate a medium exchange.'),
|
|
||||||
'error'
|
|
||||||
)
|
|
||||||
with transaction.atomic():
|
|
||||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
|
||||||
medium = perform_media_exchange(
|
|
||||||
organizer=request.organizer,
|
|
||||||
media_type=exchange_medium_type,
|
|
||||||
identifier=exchange_medium_identifier,
|
|
||||||
link_orderposition=op,
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
)
|
|
||||||
source_type = medium.media_type.identifier
|
|
||||||
checkin_args['reusable_medium'] = medium
|
|
||||||
perform_checkin(**checkin_args)
|
|
||||||
else:
|
|
||||||
perform_checkin(**checkin_args)
|
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
'status': 'incomplete',
|
'status': 'incomplete',
|
||||||
@@ -870,18 +834,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
],
|
],
|
||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except RequiredMediaExchangeError as e:
|
|
||||||
return Response({
|
|
||||||
'status': 'exchange',
|
|
||||||
'require_attention': op.require_checkin_attention,
|
|
||||||
'checkin_texts': op.checkin_texts,
|
|
||||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
|
||||||
'media_policy': e.media_policy,
|
|
||||||
'media_type': e.media_type,
|
|
||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
|
||||||
'reason': e.code,
|
|
||||||
'reason_explanation': e.msg,
|
|
||||||
}, status=400)
|
|
||||||
except CheckInError as e:
|
except CheckInError as e:
|
||||||
if not simulate:
|
if not simulate:
|
||||||
op.order.log_action('pretix.event.checkin.denied', data={
|
op.order.log_action('pretix.event.checkin.denied', data={
|
||||||
@@ -1069,9 +1021,6 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
canceled_supported=True,
|
canceled_supported=True,
|
||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
legacy_url_support=False,
|
legacy_url_support=False,
|
||||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
|
||||||
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
|
||||||
simulate=s.validated_data.get('simulate'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
@@ -65,7 +64,6 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'limit_sales_channels',
|
'limit_sales_channels',
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -80,7 +78,6 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -90,7 +87,6 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('You cannot delete this discount because it already has '
|
raise PermissionDenied('You cannot delete this discount because it already has '
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ from rest_framework.exceptions import (
|
|||||||
NotFound, PermissionDenied, ValidationError,
|
NotFound, PermissionDenied, ValidationError,
|
||||||
)
|
)
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.mixins import UpdateModelMixin
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.auth.permission import EventCRUDPermission
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
@@ -257,7 +256,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
copy_from = None
|
copy_from = None
|
||||||
if 'clone_from' in self.request.GET:
|
if 'clone_from' in self.request.GET:
|
||||||
@@ -321,7 +319,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
|
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
|
||||||
@@ -357,7 +354,6 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
||||||
@@ -516,7 +512,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
resp['X-Page-Generated'] = date
|
resp['X-Page-Generated'] = date
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
@@ -533,7 +528,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -543,7 +537,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
||||||
@@ -572,7 +565,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.tax_rules.all()
|
return self.request.event.tax_rules.all()
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -582,7 +574,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -592,7 +583,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
|
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
|
||||||
@@ -721,7 +711,7 @@ class SeatFilter(FilterSet):
|
|||||||
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||||
|
|
||||||
|
|
||||||
class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SeatSerializer
|
serializer_class = SeatSerializer
|
||||||
queryset = Seat.objects.none()
|
queryset = Seat.objects.none()
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'event.settings.general:write'
|
||||||
@@ -766,7 +756,6 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
|||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
serializer.instance.event.log_action(
|
serializer.instance.event.log_action(
|
||||||
@@ -776,7 +765,6 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
|||||||
data={"seats": [serializer.instance.pk]},
|
data={"seats": [serializer.instance.pk]},
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def bulk_change_blocked(self, blocked):
|
def bulk_change_blocked(self, blocked):
|
||||||
s = SeatBulkBlockInputSerializer(
|
s = SeatBulkBlockInputSerializer(
|
||||||
data=self.request.data,
|
data=self.request.data,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from celery.result import AsyncResult
|
from celery.result import AsyncResult
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -46,8 +45,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamQuerySet
|
from pretix.base.models.organizer import TeamQuerySet
|
||||||
from pretix.base.services.export import (
|
from pretix.base.services.export import (
|
||||||
ExportError, export, init_event_exporters, init_organizer_exporters,
|
export, init_event_exporters, init_organizer_exporters, multiexport,
|
||||||
multiexport,
|
|
||||||
)
|
)
|
||||||
from pretix.helpers.http import ChunkBasedFileResponse
|
from pretix.helpers.http import ChunkBasedFileResponse
|
||||||
|
|
||||||
@@ -151,11 +149,8 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
))
|
))
|
||||||
exporters = []
|
exporters = []
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
try:
|
ex._serializer = JobRunSerializer(exporter=ex)
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
exporters.append(ex)
|
||||||
exporters.append(ex)
|
|
||||||
except ExportError:
|
|
||||||
pass
|
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
@@ -185,11 +180,8 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
))
|
))
|
||||||
exporters = []
|
exporters = []
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
try:
|
ex._serializer = JobRunSerializer(exporter=ex)
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
exporters.append(ex)
|
||||||
exporters.append(ex)
|
|
||||||
except ExportError:
|
|
||||||
pass
|
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
@@ -228,7 +220,6 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
|||||||
qs = self.request.event.scheduled_exports
|
qs = self.request.event.scheduled_exports
|
||||||
return qs.select_related("owner")
|
return qs.select_related("owner")
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||||
@@ -259,7 +250,6 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
|||||||
))
|
))
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||||
@@ -285,7 +275,6 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
self.request.event.log_action(
|
self.request.event.log_action(
|
||||||
'pretix.event.export.schedule.deleted',
|
'pretix.event.export.schedule.deleted',
|
||||||
@@ -313,7 +302,6 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
qs = self.request.organizer.scheduled_exports
|
qs = self.request.organizer.scheduled_exports
|
||||||
return qs.select_related("owner")
|
return qs.select_related("owner")
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||||
@@ -344,7 +332,6 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
))
|
))
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||||
@@ -395,7 +382,6 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
'pretix.organizer.export.schedule.deleted',
|
'pretix.organizer.export.schedule.deleted',
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -48,15 +47,13 @@ from pretix.api.pagination import TotalOrderingFilter
|
|||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||||
QuestionnaireSerializer, QuestionOptionSerializer, QuestionSerializer,
|
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||||
QuotaSerializer,
|
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||||
ItemVariation, Question, QuestionOption, Quota,
|
ItemVariation, Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.models.items import Questionnaire
|
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.helpers.i18n import i18ncomp
|
from pretix.helpers.i18n import i18ncomp
|
||||||
@@ -112,7 +109,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -127,7 +123,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
@@ -144,7 +139,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
|
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
|
||||||
@@ -189,7 +183,6 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
item = self.item
|
item = self.item
|
||||||
if not item.has_variations:
|
if not item.has_variations:
|
||||||
@@ -204,7 +197,6 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.item.log_action(
|
serializer.instance.item.log_action(
|
||||||
@@ -215,7 +207,6 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
|
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
|
||||||
@@ -258,7 +249,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['item'] = self.item
|
ctx['item'] = self.item
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||||
serializer.save(base_item=item)
|
serializer.save(base_item=item)
|
||||||
@@ -269,7 +259,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.base_item.log_action(
|
serializer.instance.base_item.log_action(
|
||||||
@@ -279,7 +268,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
instance.base_item.log_action(
|
instance.base_item.log_action(
|
||||||
@@ -315,7 +303,6 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['item'] = self.item
|
ctx['item'] = self.item
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||||
serializer.save(item=item)
|
serializer.save(item=item)
|
||||||
@@ -326,7 +313,6 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.item.log_action(
|
serializer.instance.item.log_action(
|
||||||
@@ -336,7 +322,6 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
instance.item.log_action(
|
instance.item.log_action(
|
||||||
@@ -369,7 +354,6 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['item'] = self.item
|
ctx['item'] = self.item
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
item = self.item
|
item = self.item
|
||||||
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
||||||
@@ -381,7 +365,6 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.base_item.log_action(
|
serializer.instance.base_item.log_action(
|
||||||
@@ -391,7 +374,6 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
instance.base_item.log_action(
|
instance.base_item.log_action(
|
||||||
@@ -421,7 +403,6 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.categories.all()
|
return self.request.event.categories.all()
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -436,7 +417,6 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -446,7 +426,6 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
for item in instance.items.all():
|
for item in instance.items.all():
|
||||||
item.category = None
|
item.category = None
|
||||||
@@ -477,12 +456,8 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
write_permission = 'event.items:write'
|
write_permission = 'event.items:write'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.questions.filter(
|
return self.request.event.questions.prefetch_related('options').all()
|
||||||
# the container_type parameter is undocumented, this API is going to change in a later release
|
|
||||||
container_type=self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION),
|
|
||||||
).prefetch_related('options').all()
|
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -497,7 +472,6 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -507,7 +481,6 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.question.deleted',
|
'pretix.event.question.deleted',
|
||||||
@@ -536,7 +509,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||||
serializer.save(question=q)
|
serializer.save(question=q)
|
||||||
@@ -547,7 +519,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.question.log_action(
|
serializer.instance.question.log_action(
|
||||||
@@ -557,7 +528,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
instance.question.log_action(
|
instance.question.log_action(
|
||||||
'pretix.event.question.option.deleted',
|
'pretix.event.question.option.deleted',
|
||||||
@@ -568,51 +538,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|
||||||
serializer_class = QuestionnaireSerializer
|
|
||||||
queryset = Questionnaire.objects.none()
|
|
||||||
#filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
|
||||||
#filterset_class = QuestionFilter
|
|
||||||
ordering_fields = ('id', 'position')
|
|
||||||
ordering = ('position', 'id')
|
|
||||||
permission = None
|
|
||||||
write_permission = 'event.items:write'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.request.event.questionnaires.prefetch_related('children').all()
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(event=self.request.event)
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.event.questionnaire.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=self.request.data
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['event'] = self.request.event
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
serializer.save(event=self.request.event)
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.event.questionnaire.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=self.request.data
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
instance.log_action(
|
|
||||||
'pretix.event.questionnaire.deleted',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
)
|
|
||||||
super().perform_destroy(instance)
|
|
||||||
|
|
||||||
|
|
||||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -661,7 +586,6 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -684,7 +608,6 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ctx['request'] = self.request
|
ctx['request'] = self.request
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
@@ -740,7 +663,6 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
serializer.instance.rebuild_cache()
|
serializer.instance.rebuild_cache()
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.quota.deleted',
|
'pretix.event.quota.deleted',
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return Response({"result": None})
|
return Response({"result": None})
|
||||||
|
|
||||||
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some performance
|
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
|
||||||
def list(self, request, **kwargs):
|
def list(self, request, **kwargs):
|
||||||
date = serializers.DateTimeField().to_representation(now())
|
date = serializers.DateTimeField().to_representation(now())
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|||||||
@@ -1658,7 +1658,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
count_waitinglist=False,
|
count_waitinglist=False,
|
||||||
force=request.data.get('force', False),
|
force=request.data.get('force', False),
|
||||||
send_mail=send_mail,
|
send_mail=send_mail,
|
||||||
ignore_date=request.data.get('force', False),
|
|
||||||
)
|
)
|
||||||
except Quota.QuotaExceededException:
|
except Quota.QuotaExceededException:
|
||||||
pass
|
pass
|
||||||
@@ -1694,8 +1693,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
count_waitinglist=False,
|
count_waitinglist=False,
|
||||||
send_mail=send_mail,
|
send_mail=send_mail,
|
||||||
force=force,
|
force=force)
|
||||||
ignore_date=force)
|
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except PaymentException as e:
|
except PaymentException as e:
|
||||||
|
|||||||
@@ -394,7 +394,6 @@ class TeamViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
||||||
instance.delete()
|
instance.delete()
|
||||||
@@ -694,7 +693,6 @@ class MembershipTypeViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied("Can only be deleted if unused.")
|
raise PermissionDenied("Can only be deleted if unused.")
|
||||||
@@ -835,7 +833,6 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if not instance.allow_delete():
|
if not instance.allow_delete():
|
||||||
raise PermissionDenied("Can only be deleted if unused.")
|
raise PermissionDenied("Can only be deleted if unused.")
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.db import transaction
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
@@ -63,7 +62,6 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -72,7 +70,6 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if serializer.instance.voucher:
|
if serializer.instance.voucher:
|
||||||
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
|
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
|
||||||
@@ -83,7 +80,6 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if instance.voucher:
|
if instance.voucher:
|
||||||
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
|
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.db import transaction
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
|||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
inst = serializer.save(organizer=self.request.organizer)
|
inst = serializer.save(organizer=self.request.organizer)
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
@@ -59,7 +57,6 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
|||||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
inst = serializer.save(organizer=self.request.organizer)
|
inst = serializer.save(organizer=self.request.organizer)
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
@@ -70,7 +67,6 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
'pretix.webhook.changed',
|
'pretix.webhook.changed',
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import sys
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape, format_html
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
@@ -36,23 +35,21 @@ def get_powered_by(request, safelink=True):
|
|||||||
d = gs.settings.license_check_input
|
d = gs.settings.license_check_input
|
||||||
if d.get('poweredby_name'):
|
if d.get('poweredby_name'):
|
||||||
if d.get('poweredby_url'):
|
if d.get('poweredby_url'):
|
||||||
msg = format_html(
|
msg = gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>').format(
|
||||||
gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>'),
|
|
||||||
name=d['poweredby_name'],
|
name=d['poweredby_name'],
|
||||||
a_name_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
a_name_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||||
escape(sl(d['poweredby_url'])) if safelink else escape(d['poweredby_url']),
|
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
|
||||||
)),
|
),
|
||||||
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||||
))
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = format_html(
|
msg = gettext('<a {a_attr}>powered by {name} based on pretix</a>').format(
|
||||||
gettext('<a {a_attr}>powered by {name} based on pretix</a>'),
|
|
||||||
name=d['poweredby_name'],
|
name=d['poweredby_name'],
|
||||||
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||||
))
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ def _get_or_create_server_keypair(organizer):
|
|||||||
|
|
||||||
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
||||||
payload = {
|
payload = {
|
||||||
'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
|
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
|
||||||
'aud': client.client_id,
|
'aud': client.client_id,
|
||||||
'exp': int(expires.timestamp()),
|
'exp': int(expires.timestamp()),
|
||||||
'iat': int(time.time()),
|
'iat': int(time.time()),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
|
|
||||||
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
|
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
def get_answer(op, question_identifier=None):
|
def get_answer(op, question_identifier=None):
|
||||||
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Order link"),
|
_("Order link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda order: eventreverse_absolute(
|
lambda order: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order', kwargs={
|
'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Ticket link"),
|
_("Ticket link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda op: eventreverse_absolute(
|
lambda op: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': op.order.code,
|
'order': op.order.code,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
import socket
|
import socket
|
||||||
@@ -42,7 +43,6 @@ from pretix.base.templatetags.rich_text import (
|
|||||||
markdown_compile_email, truelink_callback,
|
markdown_compile_email, truelink_callback,
|
||||||
)
|
)
|
||||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||||
from pretix.helpers.ssrf import should_block_access
|
|
||||||
|
|
||||||
from pretix.base.services.placeholders import ( # noqa
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
get_available_placeholders, PlaceholderContext
|
get_available_placeholders, PlaceholderContext
|
||||||
@@ -252,9 +252,13 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
|||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
|
|
||||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||||
is_private, msg = should_block_access(sa)
|
ip_addr = ipaddress.ip_address(sa[0])
|
||||||
if is_private:
|
if ip_addr.is_multicast:
|
||||||
raise socket.error(msg)
|
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||||
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
|
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||||
|
if ip_addr.is_private:
|
||||||
|
raise socket.error(f"Request to private address {sa[0]} blocked")
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -40,12 +40,11 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
|||||||
|
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
|
|
||||||
from ..exporter import MultiSheetListExporter, OrganizerLevelExportMixin
|
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||||
from ..models import Membership
|
|
||||||
from ..signals import register_multievent_data_exporters
|
from ..signals import register_multievent_data_exporters
|
||||||
|
|
||||||
|
|
||||||
class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||||
identifier = 'customerlist'
|
identifier = 'customerlist'
|
||||||
verbose_name = gettext_lazy('Customer accounts')
|
verbose_name = gettext_lazy('Customer accounts')
|
||||||
category = pgettext_lazy('export_category', 'Customer accounts')
|
category = pgettext_lazy('export_category', 'Customer accounts')
|
||||||
@@ -55,20 +54,13 @@ class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
|||||||
def get_required_organizer_permission(cls) -> str:
|
def get_required_organizer_permission(cls) -> str:
|
||||||
return 'organizer.customers:write'
|
return 'organizer.customers:write'
|
||||||
|
|
||||||
@property
|
|
||||||
def sheets(self):
|
|
||||||
return (
|
|
||||||
('customers', _('Customers')),
|
|
||||||
('memberships', _('Memberships')),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def additional_form_fields(self):
|
def additional_form_fields(self):
|
||||||
return OrderedDict(
|
return OrderedDict(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
def iterate_customers(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
qs = self.organizer.customers.prefetch_related('provider')
|
qs = self.organizer.customers.prefetch_related('provider')
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
@@ -117,52 +109,6 @@ class CustomerListExporter(OrganizerLevelExportMixin, MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
def iterate_memberships(self, form_data):
|
|
||||||
qs = Membership.objects.filter(
|
|
||||||
customer__organizer=self.organizer
|
|
||||||
).prefetch_related('membership_type').select_related('customer', 'granted_in', 'granted_in__order')
|
|
||||||
|
|
||||||
headers = [
|
|
||||||
_('Customer ID'),
|
|
||||||
_('External identifier'),
|
|
||||||
_('Email'),
|
|
||||||
_('Test mode'),
|
|
||||||
_('Canceled'),
|
|
||||||
_('Membership type'),
|
|
||||||
_('Purchase ticket'),
|
|
||||||
_('Start date'),
|
|
||||||
_('Start time'),
|
|
||||||
_('End date'),
|
|
||||||
_('End time'),
|
|
||||||
_('Name'),
|
|
||||||
]
|
|
||||||
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
|
|
||||||
if name_scheme and len(name_scheme['fields']) > 1:
|
|
||||||
for k, label, w in name_scheme['fields']:
|
|
||||||
headers.append(_('Name') + ': ' + str(label))
|
|
||||||
yield headers
|
|
||||||
|
|
||||||
tz = get_current_timezone()
|
|
||||||
for obj in qs:
|
|
||||||
row = [
|
|
||||||
obj.customer.identifier,
|
|
||||||
obj.customer.external_identifier,
|
|
||||||
obj.customer.email or '',
|
|
||||||
_('Yes') if obj.testmode else _('No'),
|
|
||||||
_('Yes') if obj.canceled else _('No'),
|
|
||||||
str(obj.membership_type.name),
|
|
||||||
f'{obj.granted_in.order.code}-{obj.granted_in.positionid}' if obj.granted_in else None,
|
|
||||||
obj.date_start.astimezone(tz).strftime('%Y-%m-%d'),
|
|
||||||
obj.date_start.astimezone(tz).strftime('%H:%M'),
|
|
||||||
obj.date_end.astimezone(tz).strftime('%Y-%m-%d'),
|
|
||||||
obj.date_end.astimezone(tz).strftime('%H:%M'),
|
|
||||||
obj.attendee_name or '',
|
|
||||||
]
|
|
||||||
if name_scheme and len(name_scheme['fields']) > 1:
|
|
||||||
for k, label, w in name_scheme['fields']:
|
|
||||||
row.append(obj.attendee_name_parts.get(k, ''))
|
|
||||||
yield row
|
|
||||||
|
|
||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return '{}_customers'.format(self.organizer.slug)
|
return '{}_customers'.format(self.organizer.slug)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
|
|||||||
from ...helpers import GroupConcat
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.iter import chunked_iterable
|
from ...helpers.iter import chunked_iterable
|
||||||
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
||||||
from ...multidomain.urlreverse import eventreverse_absolute
|
from ...multidomain.urlreverse import build_absolute_uri
|
||||||
from ..exporter import (
|
from ..exporter import (
|
||||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||||
)
|
)
|
||||||
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
})
|
})
|
||||||
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': op.web_secret,
|
'secret': op.web_secret,
|
||||||
'position': op.positionid
|
'position': op.positionid
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@@ -40,12 +42,13 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.metrics import pretix_failed_logins
|
from pretix.base.metrics import pretix_failed_logins
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.helpers.dicts import move_to_end
|
from pretix.helpers.dicts import move_to_end
|
||||||
from pretix.helpers.ratelimit import rate_limit
|
from pretix.helpers.http import get_client_ip
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -82,20 +85,40 @@ class LoginForm(forms.Form):
|
|||||||
else:
|
else:
|
||||||
move_to_end(self.fields, 'keep_logged_in')
|
move_to_end(self.fields, 'keep_logged_in')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def ratelimit_key(self):
|
||||||
|
if not settings.HAS_REDIS:
|
||||||
|
return None
|
||||||
|
client_ip = get_client_ip(self.request)
|
||||||
|
if not client_ip:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
client_ip = ipaddress.ip_address(client_ip)
|
||||||
|
except ValueError:
|
||||||
|
# Web server not set up correctly
|
||||||
|
return None
|
||||||
|
if client_ip.is_private:
|
||||||
|
# This is the private IP of the server, web server not set up correctly
|
||||||
|
return None
|
||||||
|
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||||
rate_limit_kwargs = dict(include_ip_from_request=self.request, max_num=10, expire_time=300)
|
if self.ratelimit_key:
|
||||||
if rate_limit("login", **rate_limit_kwargs, increase=False):
|
from django_redis import get_redis_connection
|
||||||
# Check rate limit without counting up, we increase below only on failed logins
|
rc = get_redis_connection("redis")
|
||||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
cnt = rc.get(self.ratelimit_key)
|
||||||
logger.info("Backend login rejected due to rate limit.")
|
if cnt and int(cnt) > 10:
|
||||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||||
|
logger.info("Backend login rejected due to rate limit.")
|
||||||
|
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||||
if self.user_cache is None:
|
if self.user_cache is None:
|
||||||
|
if self.ratelimit_key:
|
||||||
|
rc.incr(self.ratelimit_key)
|
||||||
|
rc.expire(self.ratelimit_key, 300)
|
||||||
logger.info("Backend login invalid.")
|
logger.info("Backend login invalid.")
|
||||||
pretix_failed_logins.inc(1, reason="invalid")
|
pretix_failed_logins.inc(1, reason="invalid")
|
||||||
# Count towards rate limit (result is ignored, we are checking above)
|
|
||||||
rate_limit("login", **rate_limit_kwargs)
|
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['invalid_login'],
|
self.error_messages['invalid_login'],
|
||||||
code='invalid_login'
|
code='invalid_login'
|
||||||
|
|||||||
+438
-513
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
@@ -45,7 +46,6 @@ from pytz import common_timezones
|
|||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.control.forms import SingleLanguageWidget
|
from pretix.control.forms import SingleLanguageWidget
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
from pretix.helpers.ratelimit import rate_limit
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
@@ -128,11 +128,16 @@ class UserPasswordChangeForm(forms.Form):
|
|||||||
def clean_old_pw(self):
|
def clean_old_pw(self):
|
||||||
old_pw = self.cleaned_data.get('old_pw')
|
old_pw = self.cleaned_data.get('old_pw')
|
||||||
|
|
||||||
if rate_limit("pwchange", self.user.pk, max_num=10, expire_time=300):
|
if settings.HAS_REDIS:
|
||||||
raise forms.ValidationError(
|
from django_redis import get_redis_connection
|
||||||
self.error_messages['rate_limit'],
|
rc = get_redis_connection("redis")
|
||||||
code='rate_limit',
|
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
||||||
)
|
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
||||||
|
if cnt > 10:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['rate_limit'],
|
||||||
|
code='rate_limit',
|
||||||
|
)
|
||||||
|
|
||||||
if not check_password(old_pw, self.user.password):
|
if not check_password(old_pw, self.user.password):
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
@@ -170,35 +175,19 @@ class UserEmailChangeForm(forms.Form):
|
|||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this email address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
|
||||||
}
|
}
|
||||||
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
||||||
new_email = forms.EmailField(label=_('New email address'))
|
new_email = forms.EmailField(label=_('New email address'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
self.request = kwargs.pop('request')
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_new_email(self):
|
def clean_new_email(self):
|
||||||
email = self.cleaned_data['new_email']
|
email = self.cleaned_data['new_email']
|
||||||
|
|
||||||
if rate_limit("emailchange_attempt", include_ip_from_request=self.request, max_num=5, expire_time=300):
|
|
||||||
# Rate limit lookup for conflicting email addresses to make enumeration harder
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages['rate_limit'],
|
|
||||||
code='rate_limit',
|
|
||||||
)
|
|
||||||
|
|
||||||
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['duplicate_identifier'],
|
self.error_messages['duplicate_identifier'],
|
||||||
code='duplicate_identifier',
|
code='duplicate_identifier',
|
||||||
)
|
)
|
||||||
|
|
||||||
if rate_limit("emailchange", self.user.pk, max_num=2, expire_time=300):
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages['rate_limit'],
|
|
||||||
code='rate_limit',
|
|
||||||
)
|
|
||||||
return email
|
return email
|
||||||
|
|||||||
@@ -43,11 +43,6 @@ from django.utils.timezone import get_current_timezone, now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.helpers.format import PlainHtmlAlternativeString
|
from pretix.helpers.format import PlainHtmlAlternativeString
|
||||||
from pretix.helpers.i18n import (
|
|
||||||
get_format_without_seconds, get_javascript_format,
|
|
||||||
get_javascript_format_without_seconds,
|
|
||||||
)
|
|
||||||
from pretix.helpers.safedownload import get_token
|
|
||||||
|
|
||||||
|
|
||||||
def replace_arabic_numbers(inp):
|
def replace_arabic_numbers(inp):
|
||||||
@@ -113,7 +108,7 @@ class DatePickerWidget(forms.DateInput):
|
|||||||
|
|
||||||
|
|
||||||
class TimePickerWidget(forms.TimeInput):
|
class TimePickerWidget(forms.TimeInput):
|
||||||
def __init__(self, attrs=None, time_format=None, without_seconds=False):
|
def __init__(self, attrs=None, time_format=None):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
if 'placeholder' in attrs:
|
if 'placeholder' in attrs:
|
||||||
del attrs['placeholder']
|
del attrs['placeholder']
|
||||||
@@ -122,27 +117,8 @@ class TimePickerWidget(forms.TimeInput):
|
|||||||
time_attrs['class'] += ' timepickerfield'
|
time_attrs['class'] += ' timepickerfield'
|
||||||
time_attrs['autocomplete'] = 'off'
|
time_attrs['autocomplete'] = 'off'
|
||||||
|
|
||||||
if time_format or without_seconds:
|
|
||||||
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
|
||||||
def time_format_attr():
|
|
||||||
if without_seconds:
|
|
||||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
|
|
||||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
|
||||||
|
|
||||||
def time_format_attr():
|
|
||||||
if without_seconds:
|
|
||||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
|
|
||||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
|
||||||
|
|
||||||
def placeholder():
|
def placeholder():
|
||||||
if without_seconds:
|
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||||
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
|
||||||
else:
|
|
||||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
).strftime(tf)
|
).strftime(tf)
|
||||||
@@ -158,26 +134,36 @@ class TimePickerWidget(forms.TimeInput):
|
|||||||
|
|
||||||
class UploadedFileWidget(forms.ClearableFileInput):
|
class UploadedFileWidget(forms.ClearableFileInput):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.position = kwargs.pop('position')
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
self.answer = kwargs.pop('answer')
|
self.answer = kwargs.pop('answer')
|
||||||
self.request = kwargs.pop('request')
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
class FakeFile:
|
class FakeFile:
|
||||||
def __init__(self, file, answer, request):
|
def __init__(self, file, position, event, answer):
|
||||||
self.file = file
|
self.file = file
|
||||||
|
self.position = position
|
||||||
|
self.event = event
|
||||||
self.answer = answer
|
self.answer = answer
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return os.path.basename(self.file.name).split('.', 1)[-1]
|
return os.path.basename(self.file.name).split('.', 1)[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
token = get_token(self.request, self.answer)
|
from pretix.base.models import OrderPosition
|
||||||
if self.request.resolver_match.namespace == 'control':
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
return self.answer.backend_file_url + '?token=' + token
|
|
||||||
|
if isinstance(self.position, OrderPosition):
|
||||||
|
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
||||||
|
'order': self.position.order.code,
|
||||||
|
'secret': self.position.order.secret,
|
||||||
|
'answer': self.answer.pk,
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
return self.answer.frontend_file_url + '?token=' + token
|
return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
|
||||||
|
'answer': self.answer.pk,
|
||||||
|
})
|
||||||
|
|
||||||
def get_context(self, name, value, attrs):
|
def get_context(self, name, value, attrs):
|
||||||
# Browsers can't recognize that the server already has a file uploaded
|
# Browsers can't recognize that the server already has a file uploaded
|
||||||
@@ -190,13 +176,13 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
|||||||
|
|
||||||
def format_value(self, value):
|
def format_value(self, value):
|
||||||
if self.is_initial(value):
|
if self.is_initial(value):
|
||||||
return self.FakeFile(value, self.answer, self.request)
|
return self.FakeFile(value, self.position, self.event, self.answer)
|
||||||
|
|
||||||
|
|
||||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||||
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
||||||
|
|
||||||
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None, without_seconds=False):
|
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
if 'placeholder' in attrs:
|
if 'placeholder' in attrs:
|
||||||
del attrs['placeholder']
|
del attrs['placeholder']
|
||||||
@@ -219,36 +205,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
|||||||
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
||||||
).isoformat()
|
).isoformat()
|
||||||
|
|
||||||
if date_format or time_format or without_seconds:
|
|
||||||
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
|
||||||
def date_format_attr():
|
|
||||||
if without_seconds:
|
|
||||||
return get_javascript_format_without_seconds(date_format or "DATE_INPUT_FORMATS")
|
|
||||||
return get_javascript_format(date_format or "DATE_INPUT_FORMATS")
|
|
||||||
|
|
||||||
date_attrs['data-format'] = lazy(date_format_attr, str)
|
|
||||||
|
|
||||||
def time_format_attr():
|
|
||||||
if without_seconds:
|
|
||||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
|
||||||
|
|
||||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
|
||||||
|
|
||||||
def date_placeholder():
|
def date_placeholder():
|
||||||
if without_seconds:
|
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||||
df = date_format or get_format_without_seconds('DATE_INPUT_FORMATS')
|
|
||||||
else:
|
|
||||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||||
).strftime(df)
|
).strftime(df)
|
||||||
|
|
||||||
def time_placeholder():
|
def time_placeholder():
|
||||||
if without_seconds:
|
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||||
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
|
||||||
else:
|
|
||||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
).strftime(tf)
|
).strftime(tf)
|
||||||
@@ -305,18 +269,3 @@ class BusinessBooleanRadio(forms.RadioSelect):
|
|||||||
'False': False,
|
'False': False,
|
||||||
False: False,
|
False: False,
|
||||||
}.get(value)
|
}.get(value)
|
||||||
|
|
||||||
|
|
||||||
class OptionAttrsSelect(forms.Select):
|
|
||||||
def __init__(self, *args, option_attrs=None, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.option_attrs = option_attrs or {}
|
|
||||||
|
|
||||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
|
||||||
option = super().create_option(
|
|
||||||
name, value, label, selected, index, subindex=subindex, attrs=attrs
|
|
||||||
)
|
|
||||||
extra = self.option_attrs.get(str(value))
|
|
||||||
if extra:
|
|
||||||
option["attrs"].update(extra)
|
|
||||||
return option
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import unicodedata
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -56,8 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
|
|||||||
from pretix.base.signals import register_invoice_renderers
|
from pretix.base.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import (
|
from pretix.helpers.reportlab import (
|
||||||
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
|
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||||
normalize_text, register_ttf_font_if_new, reshaper,
|
reshaper,
|
||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -257,8 +259,18 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||||
|
|
||||||
def _normalize(self, text):
|
def _normalize(self, text):
|
||||||
# alias kept for plugin compatibility
|
# reportlab does not support unicode combination characters
|
||||||
return normalize_text(text)
|
# It's important we do this before we use ArabicReshaper
|
||||||
|
text = unicodedata.normalize("NFKC", text)
|
||||||
|
|
||||||
|
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||||
|
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||||
|
try:
|
||||||
|
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
|
||||||
|
except:
|
||||||
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
def _upper(self, val):
|
def _upper(self, val):
|
||||||
# We uppercase labels, but not in every language
|
# We uppercase labels, but not in every language
|
||||||
@@ -339,15 +351,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||||
|
|
||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
|
return self._normalize(bleach.clean(
|
||||||
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
|
text,
|
||||||
# tags.
|
tags=set(tags) if tags else set()
|
||||||
return self._normalize(
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
bleach.clean(
|
|
||||||
text,
|
|
||||||
tags=set(tags) if tags else set()
|
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PaidMarker(Flowable):
|
class PaidMarker(Flowable):
|
||||||
@@ -398,7 +405,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_to_top = 52 * mm
|
invoice_to_top = 52 * mm
|
||||||
|
|
||||||
def _draw_invoice_to(self, canvas):
|
def _draw_invoice_to(self, canvas):
|
||||||
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
|
||||||
|
style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||||
@@ -409,8 +417,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_from_top = 17 * mm
|
invoice_from_top = 17 * mm
|
||||||
|
|
||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
self.invoice.full_invoice_from,
|
self._clean_text(self.invoice.full_invoice_from),
|
||||||
style=self.stylesheet['InvoiceFrom']
|
style=self.stylesheet['InvoiceFrom']
|
||||||
)
|
)
|
||||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||||
@@ -540,12 +548,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
txt = bleach.clean(txt, tags=set()).strip()
|
||||||
|
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
@@ -563,7 +572,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
else:
|
else:
|
||||||
p_str = shorten(self.invoice.event.name)
|
p_str = shorten(self.invoice.event.name)
|
||||||
|
|
||||||
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||||
@@ -636,37 +645,39 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
||||||
if type_info_text:
|
if type_info_text:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
type_info_text,
|
type_info_text,
|
||||||
self.stylesheet['WarningBlock']
|
self.stylesheet['WarningBlock']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.custom_field:
|
if self.invoice.custom_field:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
'{}: {}'.format(
|
'{}: {}'.format(
|
||||||
str(self.invoice.event.settings.invoice_address_custom_field),
|
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
|
||||||
self.invoice.custom_field,
|
self._clean_text(self.invoice.custom_field),
|
||||||
),
|
),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||||
reference=self.invoice.internal_reference,
|
reference=self._clean_text(self.invoice.internal_reference),
|
||||||
),
|
)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_vat_id:
|
if self.invoice.invoice_to_vat_id:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
|
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_beneficiary:
|
if self.invoice.invoice_to_beneficiary:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
|
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -696,11 +707,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story = [
|
story = [
|
||||||
NextPageTemplate('FirstPage'),
|
NextPageTemplate('FirstPage'),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
(
|
self._normalize(
|
||||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||||
else pgettext('invoice', 'Invoice')
|
else pgettext('invoice', 'Invoice')
|
||||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
|
||||||
self.stylesheet['Heading1']
|
self.stylesheet['Heading1']
|
||||||
),
|
),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
@@ -722,17 +733,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
]
|
]
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
|
|
||||||
def _group_key(line):
|
def _group_key(line):
|
||||||
@@ -769,8 +780,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
max_height = self.stylesheet['Normal'].leading * 5
|
max_height = self.stylesheet['Normal'].leading * 5
|
||||||
p_style = self.stylesheet['Normal']
|
p_style = self.stylesheet['Normal']
|
||||||
for __ in range(1000):
|
for __ in range(1000):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
curr_description,
|
self._clean_text(curr_description, tags=['br']),
|
||||||
p_style
|
p_style
|
||||||
)
|
)
|
||||||
h = p.wrap(max_width, doc.height)[1]
|
h = p.wrap(max_width, doc.height)[1]
|
||||||
@@ -851,7 +862,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
# Group together at the end of the invoice
|
# Group together at the end of the invoice
|
||||||
request_show_service_date = period_line
|
request_show_service_date = period_line
|
||||||
elif period_line:
|
elif period_line:
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
period_line,
|
period_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -863,7 +874,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net_price=money_filter(net_value, self.invoice.event.currency),
|
net_price=money_filter(net_value, self.invoice.event.currency),
|
||||||
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
single_price_line,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -872,11 +883,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
localize(tax_rate) + " %",
|
localize(tax_rate) + " %",
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -893,14 +904,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
||||||
price=money_filter(gross_value, self.invoice.event.currency),
|
price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
single_price_line,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
tdata.append((
|
tdata.append((
|
||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -933,12 +944,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -947,12 +958,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
pending_sum = self.invoice.order.pending_sum
|
pending_sum = self.invoice.order.pending_sum
|
||||||
if pending_sum != total:
|
if pending_sum != total:
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum, self.invoice.event.currency)]
|
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -969,12 +980,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
s=Sum('amount')
|
s=Sum('amount')
|
||||||
)['s'] or Decimal('0.00')
|
)['s'] or Decimal('0.00')
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -997,14 +1008,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
story.append(Spacer(1, 10 * mm))
|
story.append(Spacer(1, 10 * mm))
|
||||||
|
|
||||||
if request_show_service_date:
|
if request_show_service_date:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
|
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(FontFallbackParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
|
self._normalize(self.invoice.payment_provider_text),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1028,10 +1039,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||||
]
|
]
|
||||||
thead = [
|
thead = [
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
tdata = [thead]
|
tdata = [thead]
|
||||||
@@ -1042,7 +1053,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
money_filter(gross - tax, self.invoice.event.currency),
|
money_filter(gross - tax, self.invoice.event.currency),
|
||||||
money_filter(gross, self.invoice.event.currency),
|
money_filter(gross, self.invoice.event.currency),
|
||||||
money_filter(tax, self.invoice.event.currency),
|
money_filter(tax, self.invoice.event.currency),
|
||||||
@@ -1061,7 +1072,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
story.append(Spacer(5 * mm, 5 * mm))
|
story.append(Spacer(5 * mm, 5 * mm))
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||||
table
|
table
|
||||||
]))
|
]))
|
||||||
|
|
||||||
@@ -1078,7 +1089,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net = gross - tax
|
net = gross - tax
|
||||||
|
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
fmt(net), fmt(gross), fmt(tax), ''
|
fmt(net), fmt(gross), fmt(tax), ''
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1087,13 +1098,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
Spacer(1, height=2 * mm),
|
Spacer(1, height=2 * mm),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
pgettext(
|
self._normalize(pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, this corresponds to:'
|
'{date}, this corresponds to:'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
),
|
),
|
||||||
Spacer(1, height=3 * mm),
|
Spacer(1, height=3 * mm),
|
||||||
@@ -1102,14 +1113,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||||
story.append(Spacer(1, 5 * mm))
|
story.append(Spacer(1, 5 * mm))
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(self._normalize(
|
||||||
pgettext(
|
pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, the invoice total corresponds to {total}.'
|
'{date}, the invoice total corresponds to {total}.'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
total=fmt(foreign_total)),
|
total=fmt(foreign_total))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1151,8 +1162,11 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
if not self.invoice.address_invoice_from:
|
if not self.invoice.address_invoice_from:
|
||||||
return
|
return
|
||||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
c = [
|
||||||
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
|
self._clean_text(l)
|
||||||
|
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||||
|
]
|
||||||
|
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||||
super()._draw_invoice_from(canvas)
|
super()._draw_invoice_from(canvas)
|
||||||
@@ -1211,8 +1225,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
date_format(self.invoice.date, "DATE_FORMAT"),
|
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||||
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
||||||
)
|
)
|
||||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||||
@@ -1269,7 +1283,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
|
|||||||
i = []
|
i = []
|
||||||
|
|
||||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||||
i.append(PlainTextParagraph(
|
i.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Event date: {date_range}').format(
|
pgettext('invoice', 'Event date: {date_range}').format(
|
||||||
date_range=self.invoice.event.get_date_range_display(),
|
date_range=self.invoice.event.get_date_range_display(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Do nothing. Useful for startup performance testing."
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
pass
|
|
||||||
@@ -40,7 +40,6 @@ from django.core.cache import cache
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||||
from django_querytagger.tagging import with_tag
|
|
||||||
|
|
||||||
from pretix.helpers.periodic import SKIPPED
|
from pretix.helpers.periodic import SKIPPED
|
||||||
|
|
||||||
@@ -83,8 +82,7 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
with with_tag(f"periodictask={name}"):
|
r = receiver(signal=periodic_task, sender=self)
|
||||||
r = receiver(signal=periodic_task, sender=self)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if isinstance(err, KeyboardInterrupt):
|
if isinstance(err, KeyboardInterrupt):
|
||||||
raise err
|
raise err
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ class Command(Parent):
|
|||||||
# Start the vite server in the background
|
# Start the vite server in the background
|
||||||
vite_server = subprocess.Popen(
|
vite_server = subprocess.Popen(
|
||||||
["npm", "run", "dev:control"],
|
["npm", "run", "dev:control"],
|
||||||
cwd=Path(__file__).parent.parent.parent.parent.parent,
|
cwd=Path(__file__).parent.parent.parent.parent.parent
|
||||||
stdin=subprocess.DEVNULL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
|
|||||||
+14
-20
@@ -26,7 +26,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
class BaseMediaType:
|
class BaseMediaType:
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
medium_created_from_unknown_supported = False
|
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
supports_giftcard = False
|
supports_giftcard = False
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ class BaseMediaType:
|
|||||||
def is_active(self, organizer):
|
def is_active(self, organizer):
|
||||||
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
|
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
|
||||||
|
|
||||||
def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
|
def handle_unknown(self, organizer, identifier, user, auth):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def handle_new(self, organizer, medium, user, auth):
|
def handle_new(self, organizer, medium, user, auth):
|
||||||
@@ -89,32 +88,23 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
verbose_name = _('NFC UID-based')
|
verbose_name = _('NFC UID-based')
|
||||||
icon = 'pretixbase/img/media/nfc_uid.svg'
|
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
medium_created_from_unknown_supported = True
|
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = True
|
supports_orderposition = False
|
||||||
|
|
||||||
def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
|
def handle_unknown(self, organizer, identifier, user, auth):
|
||||||
from pretix.base.models import GiftCard, ReusableMedium
|
from pretix.base.models import GiftCard, ReusableMedium
|
||||||
|
|
||||||
create_giftcard = organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool)
|
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
|
||||||
if create_giftcard or force_create:
|
|
||||||
if identifier.startswith("08"):
|
if identifier.startswith("08"):
|
||||||
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
|
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
|
||||||
# UIDs on every read, so they won't be useful.
|
# UIDs on every read, so they won't be useful.
|
||||||
return
|
return
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if create_giftcard:
|
gc = GiftCard.objects.create(
|
||||||
gc = GiftCard.objects.create(
|
issuer=organizer,
|
||||||
issuer=organizer,
|
expires=organizer.default_gift_card_expiry,
|
||||||
expires=organizer.default_gift_card_expiry,
|
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
|
||||||
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
|
)
|
||||||
)
|
|
||||||
gc.log_action(
|
|
||||||
'pretix.giftcards.created',
|
|
||||||
user=user, auth=auth,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
gc = None
|
|
||||||
m = ReusableMedium.objects.create(
|
m = ReusableMedium.objects.create(
|
||||||
type=self.identifier,
|
type=self.identifier,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
@@ -126,6 +116,10 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
'pretix.reusable_medium.created.auto',
|
'pretix.reusable_medium.created.auto',
|
||||||
user=user, auth=auth,
|
user=user, auth=auth,
|
||||||
)
|
)
|
||||||
|
gc.log_action(
|
||||||
|
'pretix.giftcards.created',
|
||||||
|
user=user, auth=auth,
|
||||||
|
)
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
@@ -135,7 +129,7 @@ class NfcMf0aesMediaType(BaseMediaType):
|
|||||||
icon = 'pretixbase/img/media/nfc_secure.svg'
|
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = True
|
supports_orderposition = False
|
||||||
|
|
||||||
def handle_new(self, organizer, medium, user, auth):
|
def handle_new(self, organizer, medium, user, auth):
|
||||||
from pretix.base.models import GiftCard
|
from pretix.base.models import GiftCard
|
||||||
|
|||||||
@@ -282,12 +282,10 @@ def metric_values():
|
|||||||
|
|
||||||
# Throwaway metrics
|
# Throwaway metrics
|
||||||
exact_tables = [
|
exact_tables = [
|
||||||
Order, Invoice, Event, Organizer
|
Order, OrderPosition, Invoice, Event, Organizer
|
||||||
]
|
]
|
||||||
for m in apps.get_models(): # Count all models
|
for m in apps.get_models(): # Count all models
|
||||||
if issubclass(m, OrderPosition):
|
if any(issubclass(m, p) for p in exact_tables):
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
|
|
||||||
elif any(issubclass(m, p) for p in exact_tables):
|
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
||||||
else:
|
else:
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
||||||
|
|||||||
+60
-197
@@ -19,10 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from urllib.parse import urlparse, urlsplit
|
from urllib.parse import urlparse, urlsplit
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
@@ -47,8 +43,6 @@ from pretix.multidomain.urlreverse import (
|
|||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_supported = None
|
_supported = None
|
||||||
|
|
||||||
|
|
||||||
@@ -71,49 +65,15 @@ def get_supported_language(requested_language, allowed_languages, default_langua
|
|||||||
return language
|
return language
|
||||||
|
|
||||||
|
|
||||||
class BaseLocaleMiddleware(MiddlewareMixin):
|
|
||||||
"""
|
|
||||||
This is a reduced LocaleMiddleware that uses only information contained in the WSGI request data
|
|
||||||
to figure out the language (cookie and browser settings). We need it to have a consistent language
|
|
||||||
for error pages that are generated from the middleware stack before we know e.g. which user is logged
|
|
||||||
in or which event is selected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def process_request(self, request: HttpRequest):
|
|
||||||
language = get_language_from_early_request(request)
|
|
||||||
translation.activate(language)
|
|
||||||
set_region(None)
|
|
||||||
request.LANGUAGE_CODE = language
|
|
||||||
timezone.deactivate()
|
|
||||||
|
|
||||||
def process_response(self, request: HttpRequest, response: HttpResponse):
|
|
||||||
language = translation.get_language()
|
|
||||||
patch_vary_headers(response, ('Accept-Language',))
|
|
||||||
if 'Content-Language' not in response:
|
|
||||||
response['Content-Language'] = language
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class LocaleMiddleware(MiddlewareMixin):
|
class LocaleMiddleware(MiddlewareMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This is the full LocaleMiddleware that uses all available information to figure out the correct
|
This middleware sets the correct locale and timezone
|
||||||
language for the request using all available sources, in this order of priority:
|
for a request.
|
||||||
|
|
||||||
- Backend: User settings
|
|
||||||
- Language cookie
|
|
||||||
- Frontend: Customer account settings
|
|
||||||
- Browser settings
|
|
||||||
- Frontend: Event/Organizer settings
|
|
||||||
- System default
|
|
||||||
|
|
||||||
It needs to run late in the middleware stack to have all information available for these steps.
|
|
||||||
For some cases, it is even ran a second time since the event is sometimes only figured out after the
|
|
||||||
middleware stack (can happen for plugin views).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_request(self, request: HttpRequest):
|
def process_request(self, request: HttpRequest):
|
||||||
language = get_language_from_request(request)
|
language = get_language_from_request(request)
|
||||||
region = None
|
|
||||||
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
|
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
|
||||||
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
|
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
|
||||||
# set and can be taken into account for the decision.
|
# set and can be taken into account for the decision.
|
||||||
@@ -134,16 +94,15 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
if '-' not in language and settings_holder.settings.region:
|
if '-' not in language and settings_holder.settings.region:
|
||||||
language += '-' + settings_holder.settings.region
|
language += '-' + settings_holder.settings.region
|
||||||
if settings_holder.settings.region:
|
if settings_holder.settings.region:
|
||||||
region = settings_holder.settings.region
|
set_region(settings_holder.settings.region)
|
||||||
else:
|
else:
|
||||||
gs = global_settings_object(request)
|
gs = global_settings_object(request)
|
||||||
if '-' not in language and gs.settings.region:
|
if '-' not in language and gs.settings.region:
|
||||||
language += '-' + gs.settings.region
|
language += '-' + gs.settings.region
|
||||||
if gs.settings.region:
|
if gs.settings.region:
|
||||||
region = gs.settings.region
|
set_region(gs.settings.region)
|
||||||
|
|
||||||
translation.activate(language)
|
translation.activate(language)
|
||||||
set_region(region)
|
|
||||||
request.LANGUAGE_CODE = get_language_without_region()
|
request.LANGUAGE_CODE = get_language_without_region()
|
||||||
|
|
||||||
tzname = None
|
tzname = None
|
||||||
@@ -223,24 +182,6 @@ def get_default_language():
|
|||||||
return settings.LANGUAGE_CODE
|
return settings.LANGUAGE_CODE
|
||||||
|
|
||||||
|
|
||||||
def get_language_from_early_request(request: HttpRequest) -> str:
|
|
||||||
"""
|
|
||||||
Analyzes the request to find what language the user wants the system to
|
|
||||||
show using only WSGI-available information. Only languages listed in
|
|
||||||
settings.LANGUAGES are taken into account. If the user requests a sublanguage
|
|
||||||
where we have a main language, we send out the main language.
|
|
||||||
"""
|
|
||||||
global _supported
|
|
||||||
if _supported is None:
|
|
||||||
_supported = OrderedDict(settings.LANGUAGES)
|
|
||||||
|
|
||||||
return (
|
|
||||||
get_language_from_cookie(request)
|
|
||||||
or get_language_from_browser(request)
|
|
||||||
or get_default_language()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_language_from_request(request: HttpRequest) -> str:
|
def get_language_from_request(request: HttpRequest) -> str:
|
||||||
"""
|
"""
|
||||||
Analyzes the request to find what language the user wants the system to
|
Analyzes the request to find what language the user wants the system to
|
||||||
@@ -255,6 +196,7 @@ def get_language_from_request(request: HttpRequest) -> str:
|
|||||||
if request.path.startswith(get_script_prefix() + 'control'):
|
if request.path.startswith(get_script_prefix() + 'control'):
|
||||||
return (
|
return (
|
||||||
get_language_from_user_settings(request)
|
get_language_from_user_settings(request)
|
||||||
|
or get_language_from_customer_settings(request)
|
||||||
or get_language_from_cookie(request)
|
or get_language_from_cookie(request)
|
||||||
or get_language_from_browser(request)
|
or get_language_from_browser(request)
|
||||||
or get_language_from_event(request)
|
or get_language_from_event(request)
|
||||||
@@ -279,26 +221,7 @@ def _parse_csp(header):
|
|||||||
return h
|
return h
|
||||||
|
|
||||||
|
|
||||||
VALID_CSP_DIRECTIVES = [
|
|
||||||
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
|
|
||||||
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
|
|
||||||
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
|
|
||||||
]
|
|
||||||
|
|
||||||
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_csp(h):
|
|
||||||
for k, v in h.items():
|
|
||||||
if k not in VALID_CSP_DIRECTIVES:
|
|
||||||
raise ValueError("Invalid CSP directive " + k)
|
|
||||||
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
|
|
||||||
logger.warning("Stripping invalid component from CSP: %r", h)
|
|
||||||
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
|
|
||||||
|
|
||||||
|
|
||||||
def _render_csp(h):
|
def _render_csp(h):
|
||||||
_sanitize_csp(h)
|
|
||||||
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
||||||
|
|
||||||
|
|
||||||
@@ -314,51 +237,25 @@ def _merge_csp(a, b):
|
|||||||
for k, v in a.items():
|
for k, v in a.items():
|
||||||
if "'unsafe-inline'" in v:
|
if "'unsafe-inline'" in v:
|
||||||
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
||||||
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha256-")]
|
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
|
||||||
|
|
||||||
|
|
||||||
def add_to_response_csp(response, csp_to_merge):
|
|
||||||
if "Content-Security-Policy" in response:
|
|
||||||
csp = _parse_csp(response["Content-Security-Policy"])
|
|
||||||
else:
|
|
||||||
csp = {}
|
|
||||||
|
|
||||||
_merge_csp(csp, csp_to_merge)
|
|
||||||
|
|
||||||
if csp:
|
|
||||||
response["Content-Security-Policy"] = _render_csp(csp)
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_response_csp_via_request(request, csp_to_merge):
|
|
||||||
_merge_csp(request._csp_to_merge, csp_to_merge)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_csp_hash(data):
|
|
||||||
hash_str = base64.b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode("ascii")
|
|
||||||
return f"'sha256-{hash_str}'"
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityMiddleware(MiddlewareMixin):
|
class SecurityMiddleware(MiddlewareMixin):
|
||||||
SAFE_TYPES = (
|
CSP_EXEMPT = (
|
||||||
# CSP policies are only used for:
|
'/api/v1/docs/',
|
||||||
# - HTML and SVG in top-level contexts
|
|
||||||
# - SVG or JS Workers delivered in embedded contexts
|
|
||||||
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
|
|
||||||
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
|
|
||||||
# on API responses or CSS. We do however include it with other types as a precaution
|
|
||||||
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
|
|
||||||
# we ever add service workers to not break the protection of this feature:
|
|
||||||
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
|
|
||||||
'application/json',
|
|
||||||
'text/css',
|
|
||||||
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
|
|
||||||
# but at the moment it does not seem to be an issue to just send it.
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
request._csp_to_merge = {}
|
|
||||||
|
|
||||||
def process_response(self, request, resp):
|
def process_response(self, request, resp):
|
||||||
|
def nested_dict_values(d):
|
||||||
|
for v in d.values():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
yield from nested_dict_values(v)
|
||||||
|
else:
|
||||||
|
if isinstance(v, str):
|
||||||
|
yield v
|
||||||
|
|
||||||
|
url = resolve(request.path_info)
|
||||||
|
|
||||||
if settings.DEBUG and resp.status_code >= 400:
|
if settings.DEBUG and resp.status_code >= 400:
|
||||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||||
# pages
|
# pages
|
||||||
@@ -369,24 +266,18 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# https://github.com/pretix/pretix/issues/765
|
# https://github.com/pretix/pretix/issues/765
|
||||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||||
|
|
||||||
if self._needs_csp(request, resp):
|
img_src = []
|
||||||
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
gs = global_settings_object(request)
|
||||||
elif 'Content-Security-Policy' in resp:
|
if gs.settings.leaflet_tiles:
|
||||||
del resp['Content-Security-Policy']
|
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||||
|
|
||||||
return resp
|
font_src = set()
|
||||||
|
if hasattr(request, 'event'):
|
||||||
def _needs_csp(self, request, resp):
|
for font in get_fonts(request.event, pdf_support_required=False).values():
|
||||||
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
|
for path in list(nested_dict_values(font)):
|
||||||
return False
|
font_location = urlparse(path)
|
||||||
|
if font_location.scheme and font_location.netloc:
|
||||||
if getattr(resp, '_csp_ignore', False):
|
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _build_csp(self, request, resp):
|
|
||||||
url = resolve(request.path_info)
|
|
||||||
|
|
||||||
h = {
|
h = {
|
||||||
'default-src': ["{static}"],
|
'default-src': ["{static}"],
|
||||||
@@ -394,9 +285,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'object-src': ["'none'"],
|
'object-src': ["'none'"],
|
||||||
'frame-src': ['{static}'],
|
'frame-src': ['{static}'],
|
||||||
'style-src': ["{static}", "{media}"],
|
'style-src': ["{static}", "{media}"],
|
||||||
'connect-src': ["{static}", "{dynamic}", "{media}"],
|
'connect-src': ["{dynamic}", "{media}"],
|
||||||
'img-src': ["{static}", "{media}", "data:"],
|
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||||
'font-src': ["{static}"],
|
'font-src': ["{static}"] + list(font_src),
|
||||||
'media-src': ["{static}", "data:"],
|
'media-src': ["{static}", "data:"],
|
||||||
# form-action is not only used to match on form actions, but also on URLs
|
# form-action is not only used to match on form actions, but also on URLs
|
||||||
# form-actions redirect to. In the context of e.g. payment providers or
|
# form-actions redirect to. In the context of e.g. payment providers or
|
||||||
@@ -405,18 +296,17 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
||||||
}
|
}
|
||||||
|
|
||||||
gs = global_settings_object(request)
|
|
||||||
if gs.settings.leaflet_tiles:
|
|
||||||
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
|
||||||
|
|
||||||
if hasattr(request, 'event'):
|
|
||||||
h['font-src'] += list(self._get_font_origins(request.event))
|
|
||||||
|
|
||||||
if settings.VITE_DEV_MODE:
|
if settings.VITE_DEV_MODE:
|
||||||
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||||
h['style-src'] += ["'unsafe-inline'"]
|
h['style-src'] += ["'unsafe-inline'"]
|
||||||
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||||
|
|
||||||
|
if hasattr(request, 'csp_nonce'):
|
||||||
|
nonce = f"'nonce-{request.csp_nonce}'"
|
||||||
|
h['script-src'].append(nonce)
|
||||||
|
if not settings.VITE_DEV_MODE:
|
||||||
|
# can't have 'unsafe-inline' and nonce at the same time
|
||||||
|
h['style-src'].append(nonce)
|
||||||
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
||||||
if (
|
if (
|
||||||
url.url_name == "event.order.pay.change" or
|
url.url_name == "event.order.pay.change" or
|
||||||
@@ -425,35 +315,27 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
h['script-src'].append('https://pay.google.com')
|
h['script-src'].append('https://pay.google.com')
|
||||||
h['frame-src'].append('https://pay.google.com')
|
h['frame-src'].append('https://pay.google.com')
|
||||||
h['connect-src'].append('https://google.com/pay')
|
h['connect-src'].append('https://google.com/pay')
|
||||||
|
|
||||||
if settings.LOG_CSP:
|
if settings.LOG_CSP:
|
||||||
h['report-uri'] = ["/csp_report/"]
|
h['report-uri'] = ["/csp_report/"]
|
||||||
|
|
||||||
if request._csp_to_merge:
|
|
||||||
_merge_csp(h, request._csp_to_merge)
|
|
||||||
|
|
||||||
if 'Content-Security-Policy' in resp:
|
if 'Content-Security-Policy' in resp:
|
||||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||||
|
|
||||||
if settings.CSP_ADDITIONAL_HEADER:
|
if settings.CSP_ADDITIONAL_HEADER:
|
||||||
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
|
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
|
||||||
|
|
||||||
placeholders = {
|
staticdomain = "'self'"
|
||||||
"{static}": ["'self'"],
|
dynamicdomain = "'self'"
|
||||||
"{dynamic}": ["'self'"],
|
mediadomain = "'self'"
|
||||||
"{media}": ["'self'"],
|
|
||||||
}
|
|
||||||
if settings.MEDIA_URL.startswith('http'):
|
if settings.MEDIA_URL.startswith('http'):
|
||||||
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
|
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||||
if settings.STATIC_URL.startswith('http'):
|
if settings.STATIC_URL.startswith('http'):
|
||||||
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
|
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||||
if settings.SITE_URL.startswith('http'):
|
if settings.SITE_URL.startswith('http'):
|
||||||
if settings.SITE_URL.find('/', 9) > 0:
|
if settings.SITE_URL.find('/', 9) > 0:
|
||||||
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||||
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||||
else:
|
else:
|
||||||
placeholders["{static}"].append(settings.SITE_URL)
|
staticdomain += " " + settings.SITE_URL
|
||||||
placeholders["{dynamic}"].append(settings.SITE_URL)
|
dynamicdomain += " " + settings.SITE_URL
|
||||||
|
|
||||||
if hasattr(request, 'organizer') and request.organizer:
|
if hasattr(request, 'organizer') and request.organizer:
|
||||||
if hasattr(request, 'event') and request.event:
|
if hasattr(request, 'event') and request.event:
|
||||||
@@ -464,29 +346,18 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||||
placeholders["{dynamic}"].append(domain)
|
dynamicdomain += " " + domain
|
||||||
|
|
||||||
for k, v in h.items():
|
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||||
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
|
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||||
|
media=mediadomain)
|
||||||
|
for k, v in h.items():
|
||||||
|
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
|
||||||
|
resp['Content-Security-Policy'] = _render_csp(h)
|
||||||
|
elif 'Content-Security-Policy' in resp:
|
||||||
|
del resp['Content-Security-Policy']
|
||||||
|
|
||||||
return h
|
return resp
|
||||||
|
|
||||||
def _get_font_origins(self, event):
|
|
||||||
def nested_dict_values(d):
|
|
||||||
for v in d.values():
|
|
||||||
if isinstance(v, dict):
|
|
||||||
yield from nested_dict_values(v)
|
|
||||||
else:
|
|
||||||
if isinstance(v, str):
|
|
||||||
yield v
|
|
||||||
|
|
||||||
font_src = set()
|
|
||||||
for font in get_fonts(event, pdf_support_required=False).values():
|
|
||||||
for path in list(nested_dict_values(font)):
|
|
||||||
font_location = urlparse(path)
|
|
||||||
if font_location.scheme and font_location.netloc:
|
|
||||||
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
|
||||||
return font_src
|
|
||||||
|
|
||||||
|
|
||||||
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
||||||
@@ -497,16 +368,8 @@ class RejectInvalidInputMiddleware(MiddlewareMixin):
|
|||||||
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
|
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
|
||||||
raise BadRequest("Invalid characters in input.")
|
raise BadRequest("Invalid characters in input.")
|
||||||
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
|
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
|
||||||
try:
|
if any("\x00" in value for key, value_list in request.POST.lists() for value in value_list):
|
||||||
post_data = request.POST.lists()
|
raise BadRequest("Invalid characters in input.")
|
||||||
except BadRequest:
|
|
||||||
# Reading request.POST wasn't possible, probably an invalid charset. Django will crash once we actually
|
|
||||||
# use request.POST, but if we don't, let's not crash it (required for some weird payment provider
|
|
||||||
# webhooks, e.g. computop).
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if any("\x00" in value for key, value_list in post_data for value in value_list):
|
|
||||||
raise BadRequest("Invalid characters in input.")
|
|
||||||
|
|
||||||
|
|
||||||
class CustomCommonMiddleware(CommonMiddleware):
|
class CustomCommonMiddleware(CommonMiddleware):
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db.models import Count
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def clean_duplicate_secrets(apps, schema_editor):
|
|
||||||
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
|
|
||||||
# unless Order.code is already too long to append something. This would need to be fixed by
|
|
||||||
# sysadmins manually.
|
|
||||||
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
|
|
||||||
Order = apps.get_model("pretixbase", "Order")
|
|
||||||
|
|
||||||
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
|
||||||
for row in qs:
|
|
||||||
affected = OrderPosition.all.filter(
|
|
||||||
**{k: v for k, v in row.items() if k != "c"}
|
|
||||||
).order_by("pk")
|
|
||||||
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
|
|
||||||
for i, a in enumerate(affected):
|
|
||||||
if i > 0:
|
|
||||||
a.secret = a.secret + "__dupl__" + str(a.pk)
|
|
||||||
logger.info(
|
|
||||||
f"Ticket {a.pk} has new secret {a.secret}"
|
|
||||||
)
|
|
||||||
a.save(update_fields=["organizer_id", "secret"])
|
|
||||||
|
|
||||||
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
|
||||||
for row in qs:
|
|
||||||
affected = Order.objects.filter(
|
|
||||||
**{k: v for k, v in row.items() if k != "c"}
|
|
||||||
).order_by("pk")
|
|
||||||
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
|
|
||||||
for i, a in enumerate(affected):
|
|
||||||
if i > 0:
|
|
||||||
if len(a.code) > 16 - len(str(a.pk)):
|
|
||||||
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
|
|
||||||
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
|
|
||||||
logger.info(
|
|
||||||
f"Order {a.pk} has new code {a.code}"
|
|
||||||
)
|
|
||||||
a.save(update_fields=["organizer_id", "code"])
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"pretixbase",
|
|
||||||
"0301_reusablemedium_remove_orderposition",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
|
|
||||||
]
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"pretixbase",
|
|
||||||
"0302_resolve_duplicate_codes_and_secrets",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunSQL(
|
|
||||||
"UPDATE pretixbase_order "
|
|
||||||
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
|
|
||||||
"WHERE pretixbase_order.organizer_id IS NULL;",
|
|
||||||
migrations.RunSQL.noop,
|
|
||||||
),
|
|
||||||
migrations.RunSQL(
|
|
||||||
"UPDATE pretixbase_orderposition "
|
|
||||||
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
|
|
||||||
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
|
|
||||||
migrations.RunSQL.noop,
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="order",
|
|
||||||
name="organizer",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="orders",
|
|
||||||
to="pretixbase.organizer",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="orderposition",
|
|
||||||
name="organizer",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="order_positions",
|
|
||||||
to="pretixbase.organizer",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-04-15 20:10
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.helpers.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0303_alter_order_organizer_alter_orderposition_organizer'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='cartposition',
|
|
||||||
name='tax_rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='invoiceline',
|
|
||||||
name='tax_rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderfee',
|
|
||||||
name='tax_rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='tax_rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='transaction',
|
|
||||||
name='tax_rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='taxrule',
|
|
||||||
name='rate',
|
|
||||||
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
|
||||||
),
|
|
||||||
|
|
||||||
]
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db import IntegrityError, migrations, transaction
|
|
||||||
from django.db.models import Count, F
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def fix_cross_organizer_eventmetavalues(apps, schema_editor):
|
|
||||||
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
|
||||||
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
|
||||||
|
|
||||||
cross_org_values = EventMetaValue.objects.filter(
|
|
||||||
event__organizer__pk__ne=F('property__organizer__pk')
|
|
||||||
).order_by('event__organizer__slug', 'event__slug')
|
|
||||||
for emv in cross_org_values:
|
|
||||||
logger.warning("%s", f"Fixing cross-organizer EventMetaValue: {emv.event.organizer.slug}/{emv.event.slug}")
|
|
||||||
logger.warning(" %s", f"{emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
|
||||||
try:
|
|
||||||
emv.property = emv.event.organizer.meta_properties.get(name=emv.property.name)
|
|
||||||
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
|
|
||||||
correct = EventMetaValue.objects.get(event=emv.event, property=emv.property)
|
|
||||||
if correct.value != emv.value:
|
|
||||||
logger.warning(" %s", f"WARN: conflicting EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
|
||||||
else:
|
|
||||||
logger.warning(" %s", f"OK: same-value EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
|
||||||
logger.warning(" %s", f"keeping: {correct.property.name}({correct.property.id}@{correct.property.organizer.slug}) = {repr(correct.value)}")
|
|
||||||
emv.delete()
|
|
||||||
else:
|
|
||||||
logger.warning(" %s", f"OK: found existing EventMetaProperty in {emv.event.organizer.slug}, updating reference")
|
|
||||||
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
|
||||||
emv.save(update_fields=["property"])
|
|
||||||
except EventMetaProperty.DoesNotExist:
|
|
||||||
meta_prop = emv.property
|
|
||||||
meta_prop.pk = None
|
|
||||||
meta_prop.organizer = emv.event.organizer
|
|
||||||
meta_prop.filter_public = False
|
|
||||||
meta_prop.save(force_insert=True)
|
|
||||||
logger.warning(" %s", f"WARN: found no matching EventMetaProperty, creating")
|
|
||||||
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
|
||||||
emv.save(update_fields=["property"])
|
|
||||||
|
|
||||||
|
|
||||||
def make_eventmetaproperties_unique(apps, schema_editor):
|
|
||||||
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
|
||||||
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
|
||||||
|
|
||||||
duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1)
|
|
||||||
for dup in duplicates:
|
|
||||||
logger.warning("%s", f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
|
|
||||||
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
|
|
||||||
|
|
||||||
target = props[0]
|
|
||||||
invalid = props[1:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
affected = EventMetaValue.objects.filter(
|
|
||||||
event__organizer=dup['organizer'], property__in=invalid
|
|
||||||
).update(
|
|
||||||
property=target
|
|
||||||
)
|
|
||||||
logger.warning("%s", f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
|
||||||
|
|
||||||
except IntegrityError as e:
|
|
||||||
logger.warning("%s", f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
|
||||||
logger.warning("%s", f" {e}")
|
|
||||||
for prop in invalid:
|
|
||||||
newname = f'{prop.name}_DUPLICATE_{prop.id}'
|
|
||||||
logger.warning("%s", f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
|
|
||||||
prop.name = newname
|
|
||||||
prop.filter_public = False
|
|
||||||
prop.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
for prop in invalid:
|
|
||||||
logger.warning("%s", f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
|
|
||||||
prop.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0304_tax_rate_decimals"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
|
|
||||||
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0305_fixup_eventmetaproperties"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name="eventmetaproperty",
|
|
||||||
unique_together={("organizer", "name")},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Generated by Django 5.2.16 on 2026-08-05 08:00
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.helpers.database
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0306_alter_eventmetaproperty_unique_together"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="DeviceLastSeen",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("last_seen", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"device",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="pretixbase.device",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="devicelastseen",
|
|
||||||
index=pretix.helpers.database.BrinIndexIgnoredOnSQLite(
|
|
||||||
models.F("last_seen"),
|
|
||||||
autosummarize=True,
|
|
||||||
name="pretixbase_device_last_seen",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Generated by Django 4.2.17 on 2025-01-01 20:25
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0307_devicelastseen"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="CheckoutSession",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("cart_id", models.CharField(max_length=255, unique=True)),
|
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("testmode", models.BooleanField(default=False)),
|
|
||||||
("session_data", models.JSONField(default=dict)),
|
|
||||||
(
|
|
||||||
"customer",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="checkout_sessions",
|
|
||||||
to="pretixbase.customer",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"event",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="checkout_sessions",
|
|
||||||
to="pretixbase.event",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"sales_channel",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="pretixbase.saleschannel",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="invoiceaddress",
|
|
||||||
name="checkout_session",
|
|
||||||
field=models.OneToOneField(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="invoice_address",
|
|
||||||
to="pretixbase.checkoutsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 5.2.15 on 2026-08-07 20:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0308_checkoutsession_invoiceaddress_checkout_session'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='question',
|
|
||||||
name='container_type',
|
|
||||||
field=models.CharField(default='P', max_length=5),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='questionanswer',
|
|
||||||
name='checkoutsession',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.checkoutsession'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='questionanswer',
|
|
||||||
name='order',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.order'),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='questionanswer',
|
|
||||||
unique_together={('cartposition', 'question'), ('checkoutsession', 'question'), ('order', 'question'), ('orderposition', 'question')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-08-19 11:31
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0309_alter_questionanswer_unique_together_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="question",
|
|
||||||
name="valid_string_length_min",
|
|
||||||
field=models.PositiveIntegerField(null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
# Generated by Django 4.2.29 on 2026-03-19 14:24
|
|
||||||
import json
|
|
||||||
from collections import namedtuple
|
|
||||||
from itertools import chain, groupby
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import i18nfield.fields
|
|
||||||
from i18nfield.strings import LazyI18nString
|
|
||||||
|
|
||||||
import pretix.base.models.base
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
FakeQuestion = namedtuple(
|
|
||||||
'FakeQuestion', 'id question position required'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_fake_questions(settings):
|
|
||||||
def b(s):
|
|
||||||
return s == 'True'
|
|
||||||
fq = []
|
|
||||||
sqo = json.loads(settings.get('system_question_order', '{}'))
|
|
||||||
_ = LazyI18nString.from_gettext
|
|
||||||
|
|
||||||
if b(settings.get('attendee_names_asked', 'True')):
|
|
||||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), b(settings.get('attendee_names_required'))))
|
|
||||||
|
|
||||||
if b(settings.get('attendee_emails_asked')):
|
|
||||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), b(settings.get('attendee_emails_required'))))
|
|
||||||
|
|
||||||
if b(settings.get('attendee_company_asked')):
|
|
||||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), b(settings.get('attendee_company_required'))))
|
|
||||||
|
|
||||||
if b(settings.get('attendee_addresses_asked')):
|
|
||||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), b(settings.get('attendee_addresses_required'))))
|
|
||||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), b(settings.get('attendee_addresses_required'))))
|
|
||||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), b(settings.get('attendee_addresses_required'))))
|
|
||||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), b(settings.get('attendee_addresses_required'))))
|
|
||||||
return fq
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_questions_forward(apps, schema_editor):
|
|
||||||
Event = apps.get_model("pretixbase", "Event")
|
|
||||||
Item = apps.get_model("pretixbase", "Item")
|
|
||||||
Question = apps.get_model("pretixbase", "Question")
|
|
||||||
Questionnaire = apps.get_model("pretixbase", "Questionnaire")
|
|
||||||
QuestionnaireChild = apps.get_model("pretixbase", "QuestionnaireChild")
|
|
||||||
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
|
||||||
|
|
||||||
def create_grouped_item_questionnaires(event, children, label_prefix, questionnaire_type):
|
|
||||||
# group by item, creating a unique questionnaire per item
|
|
||||||
item_questionnaires = (([t[3] for t in children], item_id) for item_id, children in
|
|
||||||
groupby(children, key=lambda t: t[0]))
|
|
||||||
|
|
||||||
# group again, merging all questionnaires with identical children
|
|
||||||
merged_questionnaires = groupby(sorted(item_questionnaires, key=lambda t: [q.id for q in t[0]]),
|
|
||||||
key=lambda t: t[0])
|
|
||||||
for children, iterator in merged_questionnaires:
|
|
||||||
items = [item for _c, item in iterator]
|
|
||||||
|
|
||||||
# create questionnaires and children
|
|
||||||
questionnaire = Questionnaire.objects.create(
|
|
||||||
event=event, type=questionnaire_type, position=0, all_sales_channels=True,
|
|
||||||
internal_name=label_prefix + ', '.join(str(iname or name) for (id, iname, name) in items)
|
|
||||||
)
|
|
||||||
questionnaire.items.set([id for (id, iname, name) in items])
|
|
||||||
deps = {}
|
|
||||||
for position, child in enumerate(children):
|
|
||||||
if isinstance(child, FakeQuestion):
|
|
||||||
QuestionnaireChild.objects.create(
|
|
||||||
questionnaire=questionnaire,
|
|
||||||
position=position + 1,
|
|
||||||
system_datafield=child.id,
|
|
||||||
required=child.required,
|
|
||||||
label=child.question,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
deps[child.id] = QuestionnaireChild.objects.create(
|
|
||||||
questionnaire=questionnaire,
|
|
||||||
position=position + 1,
|
|
||||||
user_datafield=child,
|
|
||||||
required=child.required,
|
|
||||||
label=child.question,
|
|
||||||
help_text=child.help_text,
|
|
||||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
|
||||||
dependency_values=child.dependency_values,
|
|
||||||
)
|
|
||||||
|
|
||||||
for event in Event.objects.iterator():
|
|
||||||
# get relevant settings
|
|
||||||
settings = {
|
|
||||||
setting.key: setting.value for setting in EventSettingsStore.objects.filter(object_id=event.id, key__in=(
|
|
||||||
'system_question_order', 'attendee_names_asked', 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required',
|
|
||||||
'attendee_company_asked', 'attendee_company_required', 'attendee_addresses_asked', 'attendee_addresses_required',
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
# get all ticket-level questions (user-defined and system provided), along with the products for which they're asked
|
|
||||||
questions = event.questions.filter(container_type='P')
|
|
||||||
children = sorted(chain((
|
|
||||||
(item, q.position, 0, q)
|
|
||||||
for q in get_fake_questions(settings)
|
|
||||||
for item in event.items.filter(personalized=True).values_list('id', 'internal_name', 'name')
|
|
||||||
), (
|
|
||||||
(item, q.position, q.id, q)
|
|
||||||
for q in questions.filter(hidden=False)
|
|
||||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
|
||||||
)), key=lambda t: (t[0], t[1]))
|
|
||||||
|
|
||||||
create_grouped_item_questionnaires(event, children, '', 'PS')
|
|
||||||
|
|
||||||
children = sorted(chain((
|
|
||||||
(item, q.position, q.id, q)
|
|
||||||
for q in questions.filter(hidden=True)
|
|
||||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
|
||||||
)), key=lambda t: (t[0], t[1]))
|
|
||||||
|
|
||||||
create_grouped_item_questionnaires(event, children, 'Hidden questions for ', 'PH')
|
|
||||||
|
|
||||||
# get all order-level questions
|
|
||||||
questions = list(event.questions.filter(container_type='O', hidden=False).order_by('position'))
|
|
||||||
if questions:
|
|
||||||
# create questionnaires and children
|
|
||||||
questionnaire = Questionnaire.objects.create(
|
|
||||||
event=event, type='OS', position=0, all_sales_channels=True,
|
|
||||||
internal_name='Per-order questions',
|
|
||||||
)
|
|
||||||
deps = {}
|
|
||||||
for position, child in enumerate(questions):
|
|
||||||
deps[child.id] = QuestionnaireChild.objects.create(
|
|
||||||
questionnaire=questionnaire,
|
|
||||||
position=position + 1,
|
|
||||||
user_datafield=child,
|
|
||||||
required=child.required,
|
|
||||||
label=child.question,
|
|
||||||
help_text=child.help_text,
|
|
||||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
|
||||||
dependency_values=child.dependency_values,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_questions_backward(apps, schema_editor):
|
|
||||||
pass # as long as we don't delete the old columns, this is a no op. after that, it gets complicated...
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0309_alter_questionanswer_unique_together_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Questionnaire',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
('internal_name', models.CharField(max_length=255)),
|
|
||||||
('type', models.CharField(max_length=5)),
|
|
||||||
('position', models.PositiveIntegerField(default=0)),
|
|
||||||
('all_sales_channels', models.BooleanField(default=True)),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questionnaires', to='pretixbase.event')),
|
|
||||||
('items', models.ManyToManyField(related_name='questionnaires', to='pretixbase.item')),
|
|
||||||
('limit_sales_channels', models.ManyToManyField(to='pretixbase.saleschannel')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='QuestionnaireChild',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
('position', models.PositiveIntegerField(default=0)),
|
|
||||||
('system_datafield', models.CharField(max_length=25, null=True)),
|
|
||||||
('required', models.BooleanField(default=False)),
|
|
||||||
('label', i18nfield.fields.I18nTextField()),
|
|
||||||
('help_text', i18nfield.fields.I18nTextField(null=True)),
|
|
||||||
('dependency_values', pretix.base.models.fields.MultiStringField(default=[])),
|
|
||||||
('dependency_question', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.questionnairechild')),
|
|
||||||
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pretixbase.questionnaire')),
|
|
||||||
('user_datafield', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='references', to='pretixbase.question')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
migrate_questions_forward,
|
|
||||||
migrate_questions_backward,
|
|
||||||
),
|
|
||||||
# TODO(questionnaires) remove old columns from Question model
|
|
||||||
]
|
|
||||||
@@ -37,8 +37,8 @@ from .invoices import Invoice, InvoiceLine, invoice_filename
|
|||||||
from .items import (
|
from .items import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||||
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
|
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
|
||||||
Questionnaire, QuestionnaireChild, QuestionOption, Quota, SubEventItem,
|
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
||||||
SubEventItemVariation, itempicture_upload_to,
|
itempicture_upload_to,
|
||||||
)
|
)
|
||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
from .mail import OutgoingMail
|
from .mail import OutgoingMail
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from django_otp.models import Device
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
from ...helpers.countries import FastCountryField
|
from ...helpers.countries import FastCountryField
|
||||||
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
||||||
@@ -373,12 +373,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
mail(
|
mail(
|
||||||
email or self.email,
|
email or self.email,
|
||||||
_('Changes to your account'),
|
_('Account information changed'),
|
||||||
'pretixcontrol/email/security_notice.txt',
|
'pretixcontrol/email/security_notice.txt',
|
||||||
{
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
'messages': msg,
|
'messages': msg,
|
||||||
'url': mainreverse_absolute('control:user.settings'),
|
'url': build_absolute_uri('control:user.settings'),
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
},
|
},
|
||||||
event=None,
|
event=None,
|
||||||
@@ -400,13 +400,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
if reason == 'email_change':
|
if reason == 'email_change':
|
||||||
msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
|
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
|
||||||
old_email=self.email, new_email=email,
|
old_email=self.email, new_email=email,
|
||||||
))
|
))
|
||||||
elif reason == 'email_verify':
|
elif reason == 'email_verify':
|
||||||
msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
|
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
|
||||||
email=self.email,
|
email=self.email,
|
||||||
instance=settings.PRETIX_INSTANCE_NAME,
|
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid confirmation code reason')
|
raise Exception('Invalid confirmation code reason')
|
||||||
@@ -419,7 +418,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
}
|
}
|
||||||
mail(
|
mail(
|
||||||
email or self.email,
|
email or self.email,
|
||||||
_('Your confirmation code'),
|
_('pretix confirmation code'),
|
||||||
'pretixcontrol/email/confirmation_code.txt',
|
'pretixcontrol/email/confirmation_code.txt',
|
||||||
{
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
@@ -463,13 +462,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
self.email,
|
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||||
_('Reset your password'),
|
|
||||||
'pretixcontrol/email/forgot.txt',
|
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'user': self,
|
||||||
'url': (mainreverse_absolute('control:auth.forgot.recover')
|
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||||
},
|
},
|
||||||
None, locale=self.locale, user=self
|
None, locale=self.locale, user=self
|
||||||
@@ -650,22 +647,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
|
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_active_staff_session(self, session_key):
|
def has_active_staff_session(self, session_key=None):
|
||||||
"""
|
"""
|
||||||
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||||
with the given session key.
|
with the given session key.
|
||||||
"""
|
"""
|
||||||
return self.get_active_staff_session(session_key) is not None
|
return self.get_active_staff_session(session_key) is not None
|
||||||
|
|
||||||
def get_active_staff_session(self, session_key):
|
def get_active_staff_session(self, session_key=None):
|
||||||
if not self.is_staff or not session_key:
|
if not self.is_staff:
|
||||||
return None
|
return None
|
||||||
if not hasattr(self, '_staff_session_cache'):
|
if not hasattr(self, '_staff_session_cache'):
|
||||||
self._staff_session_cache = {}
|
self._staff_session_cache = {}
|
||||||
if session_key not in self._staff_session_cache:
|
if session_key not in self._staff_session_cache:
|
||||||
sess = StaffSession.objects.filter(
|
qs = StaffSession.objects.filter(
|
||||||
user=self, date_end__isnull=True, session_key=session_key
|
user=self, date_end__isnull=True
|
||||||
).first()
|
)
|
||||||
|
if session_key:
|
||||||
|
qs = qs.filter(session_key=session_key)
|
||||||
|
sess = qs.first()
|
||||||
if sess:
|
if sess:
|
||||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||||
sess.date_end = now()
|
sess.date_end = now()
|
||||||
|
|||||||
@@ -346,14 +346,11 @@ class Checkin(models.Model):
|
|||||||
REASON_INCOMPLETE = 'incomplete'
|
REASON_INCOMPLETE = 'incomplete'
|
||||||
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
||||||
REASON_AMBIGUOUS = 'ambiguous'
|
REASON_AMBIGUOUS = 'ambiguous'
|
||||||
REASON_MEDIUM_INVALID = 'medium_invalid'
|
|
||||||
REASON_MEDIUM_EXISTS = 'medium_exists'
|
|
||||||
REASON_ERROR = 'error'
|
REASON_ERROR = 'error'
|
||||||
REASON_BLOCKED = 'blocked'
|
REASON_BLOCKED = 'blocked'
|
||||||
REASON_UNAPPROVED = 'unapproved'
|
REASON_UNAPPROVED = 'unapproved'
|
||||||
REASON_INVALID_TIME = 'invalid_time'
|
REASON_INVALID_TIME = 'invalid_time'
|
||||||
REASON_ANNULLED = 'annulled'
|
REASON_ANNULLED = 'annulled'
|
||||||
REASON_ALREADY_EXCHANGED = 'already_exchanged'
|
|
||||||
REASONS = (
|
REASONS = (
|
||||||
(REASON_CANCELED, _('Order canceled')),
|
(REASON_CANCELED, _('Order canceled')),
|
||||||
(REASON_INVALID, _('Unknown ticket')),
|
(REASON_INVALID, _('Unknown ticket')),
|
||||||
@@ -369,9 +366,6 @@ class Checkin(models.Model):
|
|||||||
(REASON_UNAPPROVED, _('Order not approved')),
|
(REASON_UNAPPROVED, _('Order not approved')),
|
||||||
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
||||||
(REASON_ANNULLED, _('Check-in annulled')),
|
(REASON_ANNULLED, _('Check-in annulled')),
|
||||||
(REASON_ALREADY_EXCHANGED, _('Ticket already exchanged')),
|
|
||||||
(REASON_MEDIUM_INVALID, _('Reusable medium invalid')),
|
|
||||||
(REASON_MEDIUM_EXISTS, _('Reusable medium already exists')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
successful = models.BooleanField(
|
successful = models.BooleanField(
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ from django.db.models.aggregates import Sum
|
|||||||
from django.db.models.expressions import OuterRef, Subquery
|
from django.db.models.expressions import OuterRef, Subquery
|
||||||
from django.db.models.functions.comparison import Coalesce
|
from django.db.models.functions.comparison import Coalesce
|
||||||
from django.utils.crypto import get_random_string, salted_hmac
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
@@ -168,7 +167,7 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_security_notice(self, message, email=None):
|
def send_security_notice(self, message, email=None):
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
@@ -179,7 +178,7 @@ class Customer(LoggedModel):
|
|||||||
{
|
{
|
||||||
**self.get_email_context(),
|
**self.get_email_context(),
|
||||||
'message': str(message),
|
'message': str(message),
|
||||||
'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
|
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
|
||||||
},
|
},
|
||||||
customer=self,
|
customer=self,
|
||||||
organizer=self.organizer,
|
organizer=self.organizer,
|
||||||
@@ -300,12 +299,12 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_activation_mail(self):
|
def send_activation_mail(self):
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.forms.customer import TokenGenerator
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
|
|
||||||
ctx = self.get_email_context()
|
ctx = self.get_email_context()
|
||||||
token = TokenGenerator().make_token(self)
|
token = TokenGenerator().make_token(self)
|
||||||
ctx['url'] = eventreverse_absolute(
|
ctx['url'] = build_absolute_uri(
|
||||||
self.organizer,
|
self.organizer,
|
||||||
'presale:organizer.customer.activate'
|
'presale:organizer.customer.activate'
|
||||||
) + '?id=' + self.identifier + '&token=' + token
|
) + '?id=' + self.identifier + '&token=' + token
|
||||||
@@ -396,7 +395,7 @@ class AttendeeProfile(models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else None,
|
self.country.name,
|
||||||
]
|
]
|
||||||
for a in self.answers:
|
for a in self.answers:
|
||||||
value = a.get('value')
|
value = a.get('value')
|
||||||
@@ -410,17 +409,6 @@ class AttendeeProfile(models.Model):
|
|||||||
|
|
||||||
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
|
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def answers_key_to_index(self):
|
|
||||||
return {a.get('field_name'): i for i, a in enumerate(self.answers)}
|
|
||||||
|
|
||||||
def store_answer(self, answer_dict):
|
|
||||||
k = answer_dict['field_name']
|
|
||||||
if k in self.answers_key_to_index:
|
|
||||||
self.answers[self.answers_key_to_index[k]] = answer_dict
|
|
||||||
else:
|
|
||||||
self.answers.append(answer_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_client_id():
|
def generate_client_id():
|
||||||
return get_random_string(40)
|
return get_random_string(40)
|
||||||
|
|||||||
@@ -20,13 +20,11 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import string
|
import string
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
@@ -34,7 +32,6 @@ from pretix.base.models import LoggedModel
|
|||||||
from pretix.base.permissions import (
|
from pretix.base.permissions import (
|
||||||
AnyPermissionOf, assert_valid_event_permission,
|
AnyPermissionOf, assert_valid_event_permission,
|
||||||
)
|
)
|
||||||
from pretix.helpers import BrinIndexIgnoredOnSQLite
|
|
||||||
|
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
@@ -290,27 +287,3 @@ class Device(LoggedModel):
|
|||||||
return self.get_events_with_any_permission()
|
return self.get_events_with_any_permission()
|
||||||
else:
|
else:
|
||||||
return self.organizer.events.none()
|
return self.organizer.events.none()
|
||||||
|
|
||||||
|
|
||||||
class DeviceLastSeen(models.Model):
|
|
||||||
# This is a separate model since we expect it to get A LOT of writes and PostgreSQL always
|
|
||||||
# writes full rows and then needs to update all indexes on the row, so this is going to save a
|
|
||||||
# lot of write traffic on the databse
|
|
||||||
device = models.OneToOneField("Device", on_delete=models.CASCADE, related_name="last_seen")
|
|
||||||
last_seen = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
indexes = [
|
|
||||||
BrinIndexIgnoredOnSQLite(
|
|
||||||
# BRIN indexes are highly efficient on lots of updates, especially of chronological data
|
|
||||||
# and especially if we later want to query them by range, as we likely want to.
|
|
||||||
"last_seen",
|
|
||||||
name="pretixbase_device_last_seen",
|
|
||||||
autosummarize=True
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_recent(self):
|
|
||||||
# pretixSCAN/pretixPOS sync every 5 minutes, so 7 minutes can be considered "offline"
|
|
||||||
return now() - self.last_seen < timedelta(minutes=7)
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import warnings
|
|||||||
from collections import Counter, OrderedDict, defaultdict
|
from collections import Counter, OrderedDict, defaultdict
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -80,16 +79,10 @@ from pretix.helpers.thumb import get_thumbnail
|
|||||||
from ..settings import settings_hierarkey
|
from ..settings import settings_hierarkey
|
||||||
from .organizer import Organizer, Team
|
from .organizer import Organizer, Team
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from hierarkey.proxy import HierarkeyProxy
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EventMixin:
|
class EventMixin:
|
||||||
if TYPE_CHECKING:
|
|
||||||
settings: HierarkeyProxy
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
||||||
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
|
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
|
||||||
@@ -179,12 +172,6 @@ class EventMixin:
|
|||||||
self.date_to.astimezone(tz), ("D" if short else "l")
|
self.date_to.astimezone(tz), ("D" if short else "l")
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_same_day(self):
|
|
||||||
if not self.date_to:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return self.date_from.astimezone(self.timezone).date() == self.date_to.astimezone(self.timezone).date()
|
|
||||||
|
|
||||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a formatted string containing the start date and the end date
|
Returns a formatted string containing the start date and the end date
|
||||||
@@ -238,9 +225,6 @@ class EventMixin:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self):
|
||||||
# If we get rid of the shim, verify that
|
|
||||||
# https://github.com/py-vobject/vobject/issues/117#issuecomment-5045645314
|
|
||||||
# has been released and included
|
|
||||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -658,7 +642,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
is_remote = models.BooleanField(
|
is_remote = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("This event is remote or partially remote."),
|
verbose_name=_("This event is remote or partially remote."),
|
||||||
help_text=_("This will be used to let users know if the event is in a different timezone, and to let us calculate the local time of a user."),
|
help_text=_("This will be used to let users know if the event is in a different timezone and let’s us calculate users’ local times."),
|
||||||
)
|
)
|
||||||
geo_lat = models.FloatField(
|
geo_lat = models.FloatField(
|
||||||
verbose_name=_("Latitude"),
|
verbose_name=_("Latitude"),
|
||||||
@@ -736,11 +720,11 @@ class Event(EventMixin, LoggedModel):
|
|||||||
self.settings.mail_send_order_paid_attendee = True
|
self.settings.mail_send_order_paid_attendee = True
|
||||||
self.settings.mail_send_order_approved_attendee = True
|
self.settings.mail_send_order_approved_attendee = True
|
||||||
self.settings.mail_send_order_approved_free_attendee = True
|
self.settings.mail_send_order_approved_free_attendee = True
|
||||||
self.settings.mail_send_download_reminder_attendee = True
|
self.settings.mail_text_download_reminder_attendee = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def social_image(self):
|
def social_image(self):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
img = None
|
img = None
|
||||||
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
||||||
@@ -758,7 +742,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
logger.exception(f'Failed to create thumbnail of {logo_file}')
|
logger.exception(f'Failed to create thumbnail of {logo_file}')
|
||||||
img = default_storage.url(logo_file)
|
img = default_storage.url(logo_file)
|
||||||
if img:
|
if img:
|
||||||
return urljoin(eventreverse_absolute(self, 'presale:event.index'), img)
|
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||||
|
|
||||||
def _seats(self, ignore_voucher=None):
|
def _seats(self, ignore_voucher=None):
|
||||||
from .seating import Seat
|
from .seating import Seat
|
||||||
@@ -899,8 +883,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
||||||
)
|
)
|
||||||
|
|
||||||
is_cross_organizer = other.organizer_id != self.organizer_id
|
|
||||||
|
|
||||||
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
||||||
# Plugins can create data in installed() hook based on existing data of the event.
|
# Plugins can create data in installed() hook based on existing data of the event.
|
||||||
# Calling set_active_plugins() results in defaults being created while actually data
|
# Calling set_active_plugins() results in defaults being created while actually data
|
||||||
@@ -915,7 +897,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||||
|
|
||||||
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
|
if hasattr(other, 'alternative_domain_assignment'):
|
||||||
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||||
|
|
||||||
if not self.all_sales_channels:
|
if not self.all_sales_channels:
|
||||||
@@ -929,15 +911,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
for emv in EventMetaValue.objects.filter(event=other):
|
for emv in EventMetaValue.objects.filter(event=other):
|
||||||
emv.pk = None
|
emv.pk = None
|
||||||
emv.event = self
|
emv.event = self
|
||||||
if is_cross_organizer:
|
|
||||||
try:
|
|
||||||
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
|
|
||||||
except EventMetaProperty.DoesNotExist:
|
|
||||||
meta_prop = emv.property
|
|
||||||
meta_prop.pk = None
|
|
||||||
meta_prop.organizer = self.organizer
|
|
||||||
meta_prop.save(force_insert=True)
|
|
||||||
emv.property = meta_prop
|
|
||||||
emv.save(force_insert=True)
|
emv.save(force_insert=True)
|
||||||
|
|
||||||
for fl in EventFooterLink.objects.filter(event=other):
|
for fl in EventFooterLink.objects.filter(event=other):
|
||||||
@@ -991,13 +964,13 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if i.tax_rule_id:
|
if i.tax_rule_id:
|
||||||
i.tax_rule = tax_map[i.tax_rule_id]
|
i.tax_rule = tax_map[i.tax_rule_id]
|
||||||
|
|
||||||
if i.grant_membership_type and is_cross_organizer:
|
if i.grant_membership_type and other.organizer_id != self.organizer_id:
|
||||||
i.grant_membership_type = None
|
i.grant_membership_type = None
|
||||||
|
|
||||||
i.save() # no force_insert since i.picture.save could have already inserted
|
i.save() # no force_insert since i.picture.save could have already inserted
|
||||||
i.log_action('pretix.object.cloned')
|
i.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
if require_membership_types and not is_cross_organizer:
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
i.require_membership_types.set(require_membership_types)
|
i.require_membership_types.set(require_membership_types)
|
||||||
|
|
||||||
if not i.all_sales_channels:
|
if not i.all_sales_channels:
|
||||||
@@ -1012,7 +985,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
v._prefetched_objects_cache = {}
|
v._prefetched_objects_cache = {}
|
||||||
v.save(force_insert=True)
|
v.save(force_insert=True)
|
||||||
|
|
||||||
if require_membership_types and not is_cross_organizer:
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
v.require_membership_types.set(require_membership_types)
|
v.require_membership_types.set(require_membership_types)
|
||||||
if not v.all_sales_channels:
|
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]))
|
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
@@ -1412,12 +1385,15 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
for mp in self.organizer.meta_properties.all():
|
for mp in self.organizer.meta_properties.all():
|
||||||
if mp.required and not self.meta_data.get(mp.name):
|
if mp.required and not self.meta_data.get(mp.name):
|
||||||
issues.append(format_html(
|
issues.append(
|
||||||
'<a href="{href}{href_hash}">{text}</a>',
|
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
|
||||||
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
|
property=mp.name,
|
||||||
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
a_attr='href="%s#id_prop-%d-value"' % (
|
||||||
href_hash=f'#id_prop-{mp.pk}-value',
|
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||||
))
|
mp.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
responses = event_live_issues.send(self)
|
responses = event_live_issues.send(self)
|
||||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||||
@@ -1860,7 +1836,6 @@ class EventMetaProperty(LoggedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("position", "name",)
|
ordering = ("position", "name",)
|
||||||
unique_together = ('organizer', 'name')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def choice_keys(self):
|
def choice_keys(self):
|
||||||
@@ -1894,8 +1869,6 @@ class EventMetaValue(LoggedModel):
|
|||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.event and self.event.organizer != self.property.organizer:
|
|
||||||
raise ValidationError(_("Property and event must belong to the same organizer."))
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ from django_scopes import ScopedManager
|
|||||||
|
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
from pretix.helpers.models import NormalizedDecimalField
|
|
||||||
|
|
||||||
|
|
||||||
def invoice_filename(instance, filename: str) -> str:
|
def invoice_filename(instance, filename: str) -> str:
|
||||||
@@ -451,7 +450,7 @@ class InvoiceLine(models.Model):
|
|||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
||||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||||
tax_rate = NormalizedDecimalField(max_digits=7, decimal_places=4, default=Decimal('0'))
|
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||||
tax_name = models.CharField(max_length=190)
|
tax_name = models.CharField(max_length=190)
|
||||||
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
||||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
|||||||
+59
-157
@@ -50,7 +50,7 @@ from dateutil.tz import datetime_exists
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
MaxLengthValidator, MinLengthValidator, MinValueValidator, RegexValidator,
|
MaxLengthValidator, MinValueValidator, RegexValidator,
|
||||||
)
|
)
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -452,16 +452,11 @@ class Item(LoggedModel):
|
|||||||
MEDIA_POLICY_REUSE = 'reuse'
|
MEDIA_POLICY_REUSE = 'reuse'
|
||||||
MEDIA_POLICY_NEW = 'new'
|
MEDIA_POLICY_NEW = 'new'
|
||||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||||
MEDIA_POLICY_APPEND = 'append'
|
|
||||||
MEDIA_POLICY_APPEND_OR_NEW = 'append_or_new'
|
|
||||||
MEDIA_POLICIES = (
|
MEDIA_POLICIES = (
|
||||||
(None, _("Don't use reusable media, use regular one-off tickets")),
|
(None, _("Don't use re-usable media, use regular one-off tickets")),
|
||||||
|
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
|
||||||
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
|
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
|
||||||
(MEDIA_POLICY_REUSE, _('Require an existing medium to be reused, replacing any previous tickets')),
|
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')),
|
||||||
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used, replacing any previous tickets')),
|
|
||||||
(MEDIA_POLICY_APPEND, _('Require an existing medium to be reused, adding to any previous tickets')),
|
|
||||||
(MEDIA_POLICY_APPEND_OR_NEW,
|
|
||||||
_('Require either an existing or a new medium to be used, adding to any previous tickets')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ItemQuerySetManager()
|
objects = ItemQuerySetManager()
|
||||||
@@ -774,7 +769,7 @@ class Item(LoggedModel):
|
|||||||
null=True, blank=True, max_length=16,
|
null=True, blank=True, max_length=16,
|
||||||
verbose_name=_('Reusable media policy'),
|
verbose_name=_('Reusable media policy'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'If this product should be stored on a reusable physical medium, you can attach a physical media policy. '
|
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. '
|
||||||
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
|
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
|
||||||
'renewable season tickets or re-chargeable gift card wristbands. '
|
'renewable season tickets or re-chargeable gift card wristbands. '
|
||||||
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
|
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
|
||||||
@@ -783,7 +778,7 @@ class Item(LoggedModel):
|
|||||||
media_type = models.CharField(
|
media_type = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
choices=[(None, _("Don't use reusable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
|
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
|
||||||
verbose_name=_('Reusable media type'),
|
verbose_name=_('Reusable media type'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Select the type of physical medium that should be used for this product. Note that not all media types '
|
'Select the type of physical medium that should be used for this product. Note that not all media types '
|
||||||
@@ -885,6 +880,26 @@ class Item(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
|
now_dt = now_dt or time_machine_now()
|
||||||
|
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||||
|
if not self.active:
|
||||||
|
return 'active'
|
||||||
|
elif self.available_from and self.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif self.available_until and self.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
|
||||||
|
return 'require_voucher'
|
||||||
|
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
elif self.hidden_if_item_available and self._dependency_available:
|
||||||
|
return 'hidden_if_item_available'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||||
check_quotas = set(getattr(
|
check_quotas = set(getattr(
|
||||||
self, '_subevent_quotas', # Utilize cache in product list
|
self, '_subevent_quotas', # Utilize cache in product list
|
||||||
@@ -980,11 +995,6 @@ class Item(LoggedModel):
|
|||||||
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
|
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
|
||||||
if not mt.supports_giftcard and issue_giftcard:
|
if not mt.supports_giftcard and issue_giftcard:
|
||||||
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
|
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
|
||||||
if media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
|
||||||
if not mt.medium_created_by_server and not mt.medium_created_from_unknown_supported:
|
|
||||||
raise ValidationError(_('The selected media type requires all media to be registered in the system '
|
|
||||||
'prior to their usage. Therefore, the selected media policy does not make '
|
|
||||||
'sense for this media type.'))
|
|
||||||
if issue_giftcard:
|
if issue_giftcard:
|
||||||
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
|
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
|
||||||
'gift cards for some reusable media types can be created or re-charged directly '
|
'gift cards for some reusable media types can be created or re-charged directly '
|
||||||
@@ -1393,6 +1403,22 @@ class ItemVariation(models.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
|
now_dt = now_dt or time_machine_now()
|
||||||
|
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||||
|
if not self.active:
|
||||||
|
return 'active'
|
||||||
|
elif self.available_from and self.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif self.available_until and self.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta_data(self):
|
def meta_data(self):
|
||||||
data = self.item.meta_data
|
data = self.item.meta_data
|
||||||
@@ -1569,12 +1595,10 @@ class ItemBundle(models.Model):
|
|||||||
|
|
||||||
class Question(LoggedModel):
|
class Question(LoggedModel):
|
||||||
"""
|
"""
|
||||||
A question is a data field that can be used to extend an order or a ticket by custom
|
A question is an input field that can be used to extend a ticket by custom information,
|
||||||
information, e.g. "Attendee age". To be actually useful, questions need to be added to
|
e.g. "Attendee age". The answers are found next to the position. The answers may be found
|
||||||
one or multiple Questionnaires. The answers may be found in QuestionAnswers, attached
|
in QuestionAnswers, attached to OrderPositions/CartPositions. A question can allow one of
|
||||||
to Orders, OrderPositions or CartPositions.
|
several input types, currently:
|
||||||
|
|
||||||
A question can allow one of several input types, currently:
|
|
||||||
|
|
||||||
* a number (``TYPE_NUMBER``)
|
* a number (``TYPE_NUMBER``)
|
||||||
* a one-line string (``TYPE_STRING``)
|
* a one-line string (``TYPE_STRING``)
|
||||||
@@ -1594,7 +1618,7 @@ class Question(LoggedModel):
|
|||||||
:param required: Whether answering this question is required for submitting an order including
|
:param required: Whether answering this question is required for submitting an order including
|
||||||
items associated with this question.
|
items associated with this question.
|
||||||
:type required: bool
|
:type required: bool
|
||||||
:param items: TO BE REMOVED
|
:param items: A set of ``Items`` objects that this question should be applied to
|
||||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||||
:type ask_during_checkin: bool
|
:type ask_during_checkin: bool
|
||||||
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
||||||
@@ -1608,9 +1632,6 @@ class Question(LoggedModel):
|
|||||||
:param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable.
|
:param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable.
|
||||||
:type dependency_values: list[str]
|
:type dependency_values: list[str]
|
||||||
"""
|
"""
|
||||||
class ContainerType(models.TextChoices):
|
|
||||||
ORDER = "O", _("Order")
|
|
||||||
ORDERPOSITION = "P", _("Order position")
|
|
||||||
TYPE_NUMBER = "N"
|
TYPE_NUMBER = "N"
|
||||||
TYPE_STRING = "S"
|
TYPE_STRING = "S"
|
||||||
TYPE_TEXT = "T"
|
TYPE_TEXT = "T"
|
||||||
@@ -1646,14 +1667,7 @@ class Question(LoggedModel):
|
|||||||
related_name="questions",
|
related_name="questions",
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
container_type = models.CharField(
|
|
||||||
max_length=5,
|
|
||||||
choices=ContainerType.choices,
|
|
||||||
verbose_name=_("Asked on"),
|
|
||||||
default=ContainerType.ORDERPOSITION,
|
|
||||||
)
|
|
||||||
question = I18nTextField(
|
question = I18nTextField(
|
||||||
# TODO(questionnaires) : to be renamed to 'internal_name'
|
|
||||||
verbose_name=_("Question")
|
verbose_name=_("Question")
|
||||||
)
|
)
|
||||||
identifier = models.CharField(
|
identifier = models.CharField(
|
||||||
@@ -1669,7 +1683,6 @@ class Question(LoggedModel):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
help_text = I18nTextField(
|
help_text = I18nTextField(
|
||||||
# TODO(questionnaires) : to be removed
|
|
||||||
verbose_name=_("Help text"),
|
verbose_name=_("Help text"),
|
||||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -1679,22 +1692,22 @@ class Question(LoggedModel):
|
|||||||
choices=TYPE_CHOICES,
|
choices=TYPE_CHOICES,
|
||||||
verbose_name=_("Question type")
|
verbose_name=_("Question type")
|
||||||
)
|
)
|
||||||
required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
required = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("Required question")
|
verbose_name=_("Required question")
|
||||||
)
|
)
|
||||||
items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire
|
items = models.ManyToManyField(
|
||||||
Item,
|
Item,
|
||||||
related_name='questions',
|
related_name='questions',
|
||||||
verbose_name=_("Products"),
|
verbose_name=_("Products"),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('This question will be asked to buyers of the selected products')
|
help_text=_('This question will be asked to buyers of the selected products')
|
||||||
)
|
)
|
||||||
position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild
|
position = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
verbose_name=_("Position")
|
verbose_name=_("Position")
|
||||||
)
|
)
|
||||||
ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed
|
ask_during_checkin = models.BooleanField(
|
||||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||||
default=False
|
default=False
|
||||||
@@ -1704,7 +1717,7 @@ class Question(LoggedModel):
|
|||||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
hidden = models.BooleanField( # to be removed
|
hidden = models.BooleanField(
|
||||||
verbose_name=_('Hidden question'),
|
verbose_name=_('Hidden question'),
|
||||||
help_text=_('This question will only show up in the backend.'),
|
help_text=_('This question will only show up in the backend.'),
|
||||||
default=False
|
default=False
|
||||||
@@ -1713,10 +1726,10 @@ class Question(LoggedModel):
|
|||||||
verbose_name=_('Print answer on invoices'),
|
verbose_name=_('Print answer on invoices'),
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
dependency_question = models.ForeignKey( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
dependency_question = models.ForeignKey(
|
||||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||||
)
|
)
|
||||||
dependency_values = MultiStringField(default=[]) # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
dependency_values = MultiStringField(default=[])
|
||||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
||||||
verbose_name=_('Minimum value'),
|
verbose_name=_('Minimum value'),
|
||||||
help_text=_('Currently not supported in our apps and during check-in'))
|
help_text=_('Currently not supported in our apps and during check-in'))
|
||||||
@@ -1735,11 +1748,6 @@ class Question(LoggedModel):
|
|||||||
valid_datetime_max = models.DateTimeField(null=True, blank=True,
|
valid_datetime_max = models.DateTimeField(null=True, blank=True,
|
||||||
verbose_name=_('Maximum value'),
|
verbose_name=_('Maximum value'),
|
||||||
help_text=_('Currently not supported in our apps and during check-in'))
|
help_text=_('Currently not supported in our apps and during check-in'))
|
||||||
valid_string_length_min = models.PositiveIntegerField(null=True, blank=True,
|
|
||||||
verbose_name=_('Minimum length'),
|
|
||||||
help_text=_(
|
|
||||||
'Currently not supported in our apps and during check-in'
|
|
||||||
))
|
|
||||||
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
|
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
|
||||||
verbose_name=_('Maximum length'),
|
verbose_name=_('Maximum length'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
@@ -1755,9 +1763,9 @@ class Question(LoggedModel):
|
|||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Data field")
|
verbose_name = _("Question")
|
||||||
verbose_name_plural = _("Data fields")
|
verbose_name_plural = _("Questions")
|
||||||
ordering = ('question', 'id')
|
ordering = ('position', 'id')
|
||||||
unique_together = (('event', 'identifier'),)
|
unique_together = (('event', 'identifier'),)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -1894,11 +1902,6 @@ class Question(LoggedModel):
|
|||||||
else:
|
else:
|
||||||
raise ValidationError(_('Unknown country code.'))
|
raise ValidationError(_('Unknown country code.'))
|
||||||
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
|
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
|
||||||
if self.valid_string_length_min is not None and len(answer) < self.valid_string_length_min:
|
|
||||||
raise ValidationError(MinLengthValidator.message % {
|
|
||||||
'limit_value': self.valid_string_length_min,
|
|
||||||
'show_value': len(answer)
|
|
||||||
})
|
|
||||||
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
|
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
|
||||||
raise ValidationError(MaxLengthValidator.message % {
|
raise ValidationError(MaxLengthValidator.message % {
|
||||||
'limit_value': self.valid_string_length_max,
|
'limit_value': self.valid_string_length_max,
|
||||||
@@ -1908,7 +1911,7 @@ class Question(LoggedModel):
|
|||||||
return answer
|
return answer
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_items(event, items): # TODO(questionnaires) : remove method / move to qc
|
def clean_items(event, items):
|
||||||
for item in items:
|
for item in items:
|
||||||
if event != item.event:
|
if event != item.event:
|
||||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||||
@@ -1920,8 +1923,6 @@ class Question(LoggedModel):
|
|||||||
raise ValidationError(_("The maximum date must not be before the minimum value."))
|
raise ValidationError(_("The maximum date must not be before the minimum value."))
|
||||||
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
|
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
|
||||||
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
|
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
|
||||||
if self.valid_string_length_max and self.valid_string_length_min and self.valid_string_length_min > self.valid_string_length_max:
|
|
||||||
raise ValidationError(_("The maximum length must not be shorter than the minimum length."))
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
def clean_type_change(self, old_type, new_type):
|
def clean_type_change(self, old_type, new_type):
|
||||||
@@ -1989,105 +1990,6 @@ class QuestionOption(models.Model):
|
|||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
|
|
||||||
|
|
||||||
class Questionnaire(LoggedModel):
|
|
||||||
TYPE_ORDER_SALE = "OS"
|
|
||||||
TYPE_ORDER_POSITION_SALE = "PS"
|
|
||||||
TYPE_ORDER_POSITION_ATTENDEE_ONLY = "PA"
|
|
||||||
TYPE_ORDER_POSITION_CHECKIN = "PC"
|
|
||||||
TYPE_ORDER_POSITION_HIDDEN = "PH"
|
|
||||||
TYPE_CHOICES = (
|
|
||||||
(TYPE_ORDER_SALE, _("Order-wide, before purchase")),
|
|
||||||
(TYPE_ORDER_POSITION_SALE, _("Per product, before purchase")),
|
|
||||||
(TYPE_ORDER_POSITION_ATTENDEE_ONLY, _("Per product, via attendee link")),
|
|
||||||
(TYPE_ORDER_POSITION_CHECKIN, _("Per product, at check-in")),
|
|
||||||
(TYPE_ORDER_POSITION_HIDDEN, _("Per product, hidden")),
|
|
||||||
)
|
|
||||||
event = models.ForeignKey(
|
|
||||||
Event,
|
|
||||||
related_name="questionnaires",
|
|
||||||
on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
internal_name = models.CharField(
|
|
||||||
verbose_name=_("Internal name"),
|
|
||||||
max_length=255,
|
|
||||||
)
|
|
||||||
type = models.CharField(
|
|
||||||
max_length=5,
|
|
||||||
choices=TYPE_CHOICES,
|
|
||||||
verbose_name=_("Questionnaire type")
|
|
||||||
)
|
|
||||||
items = models.ManyToManyField(
|
|
||||||
Item,
|
|
||||||
related_name='questionnaires',
|
|
||||||
verbose_name=_("Products"),
|
|
||||||
blank=True,
|
|
||||||
help_text=_('This questionnaire will be asked to buyers of the selected products')
|
|
||||||
)
|
|
||||||
position = models.PositiveIntegerField(
|
|
||||||
default=0,
|
|
||||||
verbose_name=_("Position")
|
|
||||||
)
|
|
||||||
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"),
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionnaireChild(LoggedModel):
|
|
||||||
SYSTEM_QUESTION_CHOICES = (
|
|
||||||
('attendee_name_parts', _('Attendee name')),
|
|
||||||
('attendee_email', _('Attendee email')),
|
|
||||||
('company', _('Company')),
|
|
||||||
('street', _('Street')),
|
|
||||||
('zipcode', _('ZIP code')),
|
|
||||||
('city', _('City')),
|
|
||||||
('country', _('Country')),
|
|
||||||
)
|
|
||||||
questionnaire = models.ForeignKey(
|
|
||||||
Questionnaire,
|
|
||||||
related_name="children",
|
|
||||||
on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
position = models.PositiveIntegerField(
|
|
||||||
default=0,
|
|
||||||
verbose_name=_("Position")
|
|
||||||
)
|
|
||||||
user_datafield = models.ForeignKey(
|
|
||||||
Question,
|
|
||||||
related_name="references",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
null=True, blank=True,
|
|
||||||
)
|
|
||||||
system_datafield = models.CharField(
|
|
||||||
max_length=25,
|
|
||||||
choices=SYSTEM_QUESTION_CHOICES,
|
|
||||||
null=True, blank=True,
|
|
||||||
)
|
|
||||||
required = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=_("Required question")
|
|
||||||
)
|
|
||||||
label = I18nTextField(
|
|
||||||
verbose_name=_("Question")
|
|
||||||
)
|
|
||||||
help_text = I18nTextField(
|
|
||||||
verbose_name=_("Help text"),
|
|
||||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
|
||||||
null=True, blank=True,
|
|
||||||
)
|
|
||||||
dependency_question = models.ForeignKey(
|
|
||||||
'QuestionnaireChild', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
|
||||||
)
|
|
||||||
dependency_values = MultiStringField(default=[])
|
|
||||||
|
|
||||||
|
|
||||||
class Quota(LoggedModel):
|
class Quota(LoggedModel):
|
||||||
"""
|
"""
|
||||||
A quota is a "pool of tickets". It is there to limit the number of items
|
A quota is a "pool of tickets". It is there to limit the number of items
|
||||||
@@ -2318,7 +2220,7 @@ class Quota(LoggedModel):
|
|||||||
class ItemMetaProperty(LoggedModel):
|
class ItemMetaProperty(LoggedModel):
|
||||||
"""
|
"""
|
||||||
An event can have ItemMetaProperty objects attached to define meta information fields
|
An event can have ItemMetaProperty objects attached to define meta information fields
|
||||||
for its items. This information can be reused for example in ticket layouts.
|
for its items. This information can be re-used for example in ticket layouts.
|
||||||
|
|
||||||
:param event: The event this property is defined for.
|
:param event: The event this property is defined for.
|
||||||
:type event: Event
|
:type event: Event
|
||||||
|
|||||||
@@ -129,10 +129,7 @@ class ReusableMedium(LoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return self.expires and self.expires < now()
|
return self.expires and self.expires > now()
|
||||||
|
|
||||||
def touch(self):
|
|
||||||
self.save(update_fields=['updated'])
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("identifier", "type", "organizer"),)
|
unique_together = (("identifier", "type", "organizer"),)
|
||||||
|
|||||||
+133
-252
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
|
|||||||
|
|
||||||
from ...helpers import OF_SELF
|
from ...helpers import OF_SELF
|
||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
from ...helpers.models import NormalizedDecimalField
|
|
||||||
from ...helpers.names import build_name
|
from ...helpers.names import build_name
|
||||||
from ...testutils.middleware import debugflags_var
|
from ...testutils.middleware import debugflags_var
|
||||||
from ._transactions import (
|
from ._transactions import (
|
||||||
@@ -225,6 +224,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
@@ -328,7 +329,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
default="line",
|
default="line",
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
|
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -353,60 +354,38 @@ class Order(LockModel, LoggedModel):
|
|||||||
def _transaction_key_reset(self):
|
def _transaction_key_reset(self):
|
||||||
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
|
|
||||||
# Expects to be called in a transaction
|
|
||||||
from . import (
|
|
||||||
GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not transaction.get_connection().in_atomic_block:
|
|
||||||
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
|
|
||||||
|
|
||||||
logs_create = []
|
|
||||||
for o in orders:
|
|
||||||
if not o.testmode:
|
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
|
||||||
order_gracefully_delete.send(event, order=o)
|
|
||||||
logs_create.append(o.log_action(
|
|
||||||
'pretix.event.order.deleted', user=user, auth=auth,
|
|
||||||
data={
|
|
||||||
'code': o.code,
|
|
||||||
},
|
|
||||||
save=False,
|
|
||||||
))
|
|
||||||
LogEntry.bulk_create_and_postprocess(logs_create)
|
|
||||||
|
|
||||||
voucher_ids = OrderPosition.objects.filter(
|
|
||||||
order__in=orders,
|
|
||||||
voucher__isnull=False
|
|
||||||
).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
|
|
||||||
voucher_usages = Counter(voucher_ids)
|
|
||||||
for v_id, usage_count in voucher_usages.items():
|
|
||||||
Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
|
|
||||||
|
|
||||||
GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
|
|
||||||
GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
|
|
||||||
GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
|
|
||||||
GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
|
|
||||||
Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
|
|
||||||
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
|
|
||||||
OrderPosition.all.filter(order__in=orders).delete()
|
|
||||||
OrderFee.all.filter(order__in=orders).delete()
|
|
||||||
Transaction.objects.filter(order__in=orders).delete()
|
|
||||||
OrderRefund.objects.filter(order__in=orders).delete()
|
|
||||||
OrderPayment.objects.filter(order__in=orders).delete()
|
|
||||||
if isinstance(orders, models.QuerySet):
|
|
||||||
orders.delete()
|
|
||||||
else:
|
|
||||||
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
|
|
||||||
event.cache.delete('complain_testmode_orders')
|
|
||||||
|
|
||||||
def gracefully_delete(self, user=None, auth=None):
|
def gracefully_delete(self, user=None, auth=None):
|
||||||
|
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||||
|
|
||||||
if not self.testmode:
|
if not self.testmode:
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
raise TypeError("Only test mode orders can be deleted.")
|
||||||
|
self.log_action(
|
||||||
|
'pretix.event.order.deleted', user=user, auth=auth,
|
||||||
|
data={
|
||||||
|
'code': self.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
|
order_gracefully_delete.send(self.event, order=self)
|
||||||
|
|
||||||
|
if self.status != Order.STATUS_CANCELED:
|
||||||
|
for position in self.positions.all():
|
||||||
|
if position.voucher:
|
||||||
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||||
|
|
||||||
|
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None)
|
||||||
|
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
|
||||||
|
GiftCardTransaction.objects.filter(order=self).update(order=None)
|
||||||
|
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
|
||||||
|
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
|
||||||
|
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
|
||||||
|
OrderPosition.all.filter(order=self).delete()
|
||||||
|
OrderFee.all.filter(order=self).delete()
|
||||||
|
Transaction.objects.filter(order=self).delete()
|
||||||
|
self.refunds.all().delete()
|
||||||
|
self.payments.all().delete()
|
||||||
|
self.event.cache.delete('complain_testmode_orders')
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def email_confirm_secret(self):
|
def email_confirm_secret(self):
|
||||||
return self.tagged_secret("email_confirm", 9)
|
return self.tagged_secret("email_confirm", 9)
|
||||||
@@ -508,20 +487,20 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotate_overpayments(cls, qs, results=True, refunds=True, sums=False):
|
def annotate_overpayments(cls, qs, results=True, refunds=True, sums=False):
|
||||||
payment_sum = OrderPayment.objects.with_scopes_disabled().filter(
|
payment_sum = OrderPayment.objects.filter(
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
||||||
refund_sum = OrderRefund.objects.with_scopes_disabled().filter(
|
refund_sum = OrderRefund.objects.filter(
|
||||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||||
OrderRefund.REFUND_STATE_CREATED),
|
OrderRefund.REFUND_STATE_CREATED),
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
).order_by().values('order').annotate(s=Sum('amount')).values('s')
|
||||||
external_refund = OrderRefund.objects.with_scopes_disabled().filter(
|
external_refund = OrderRefund.objects.filter(
|
||||||
state=OrderRefund.REFUND_STATE_EXTERNAL,
|
state=OrderRefund.REFUND_STATE_EXTERNAL,
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
)
|
)
|
||||||
pending_refund = OrderRefund.objects.with_scopes_disabled().filter(
|
pending_refund = OrderRefund.objects.filter(
|
||||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
|
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
)
|
)
|
||||||
@@ -626,14 +605,42 @@ class Order(LockModel, LoggedModel):
|
|||||||
self.save(update_fields=['last_modified'])
|
self.save(update_fields=['last_modified'])
|
||||||
|
|
||||||
def set_expires(self, now_dt=None, subevents=None):
|
def set_expires(self, now_dt=None, subevents=None):
|
||||||
from pretix.base.services.payment import compute_payment_deadline
|
now_dt = now_dt or now()
|
||||||
|
tz = ZoneInfo(self.event.settings.timezone)
|
||||||
|
mode = self.event.settings.get('payment_term_mode')
|
||||||
|
if mode == 'days':
|
||||||
|
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
|
||||||
|
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
||||||
|
if self.event.settings.get('payment_term_weekdays'):
|
||||||
|
if exp_by_date.weekday() == 5:
|
||||||
|
exp_by_date += timedelta(days=2)
|
||||||
|
elif exp_by_date.weekday() == 6:
|
||||||
|
exp_by_date += timedelta(days=1)
|
||||||
|
elif mode == 'minutes':
|
||||||
|
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get('payment_term_minutes', as_type=int))
|
||||||
|
else:
|
||||||
|
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
|
||||||
|
|
||||||
self.expires = compute_payment_deadline(
|
self.expires = exp_by_date
|
||||||
event=self.event,
|
|
||||||
sales_channel=self.sales_channel,
|
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
now_dt=now_dt,
|
if term_last:
|
||||||
subevents=subevents,
|
if self.event.has_subevents and subevents:
|
||||||
)
|
terms = [
|
||||||
|
term_last.datetime(se).date()
|
||||||
|
for se in subevents
|
||||||
|
]
|
||||||
|
if not terms:
|
||||||
|
return
|
||||||
|
term_last = min(terms)
|
||||||
|
else:
|
||||||
|
term_last = term_last.datetime(self.event).date()
|
||||||
|
term_last = make_aware(datetime.combine(
|
||||||
|
term_last,
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), tz)
|
||||||
|
if term_last < self.expires:
|
||||||
|
self.expires = term_last
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def tax_total(self):
|
def tax_total(self):
|
||||||
@@ -1276,9 +1283,10 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
def answerfile_name(instance, filename: str) -> str:
|
def answerfile_name(instance, filename: str) -> str:
|
||||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||||
|
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
|
||||||
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
|
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
|
||||||
org=instance.event.organizer.slug,
|
org=event.organizer.slug,
|
||||||
ev=instance.event.slug,
|
ev=event.slug,
|
||||||
secret=secret,
|
secret=secret,
|
||||||
filename=escape_uri_path(filename),
|
filename=escape_uri_path(filename),
|
||||||
)
|
)
|
||||||
@@ -1307,14 +1315,6 @@ class QuestionAnswer(models.Model):
|
|||||||
'CartPosition', null=True, blank=True,
|
'CartPosition', null=True, blank=True,
|
||||||
related_name='answers', on_delete=models.CASCADE
|
related_name='answers', on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
order = models.ForeignKey(
|
|
||||||
'Order', null=True, blank=True,
|
|
||||||
related_name='answers', on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
checkoutsession = models.ForeignKey(
|
|
||||||
'CheckoutSession', null=True, blank=True,
|
|
||||||
related_name='answers', on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
question = models.ForeignKey(
|
question = models.ForeignKey(
|
||||||
Question, related_name='answers', on_delete=models.CASCADE
|
Question, related_name='answers', on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
@@ -1330,21 +1330,16 @@ class QuestionAnswer(models.Model):
|
|||||||
objects = ScopedManager(organizer='question__event__organizer')
|
objects = ScopedManager(organizer='question__event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [
|
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
|
||||||
['orderposition', 'question'],
|
|
||||||
['cartposition', 'question'],
|
|
||||||
['order', 'question'],
|
|
||||||
['checkoutsession', 'question'],
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_file_url(self):
|
def backend_file_url(self):
|
||||||
if self.file:
|
if self.file:
|
||||||
if self.associated_order:
|
if self.orderposition:
|
||||||
return reverse('control:event.order.download.answer', kwargs={
|
return reverse('control:event.order.download.answer', kwargs={
|
||||||
'code': self.associated_order.code,
|
'code': self.orderposition.order.code,
|
||||||
'event': self.associated_order.event.slug,
|
'event': self.orderposition.order.event.slug,
|
||||||
'organizer': self.associated_order.event.organizer.slug,
|
'organizer': self.orderposition.order.event.organizer.slug,
|
||||||
'answer': self.pk,
|
'answer': self.pk,
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
@@ -1354,14 +1349,14 @@ class QuestionAnswer(models.Model):
|
|||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
|
|
||||||
if self.file:
|
if self.file:
|
||||||
if self.associated_order:
|
if self.orderposition:
|
||||||
url = eventreverse(self.associated_order.event, 'presale:event.order.download.answer', kwargs={
|
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={
|
||||||
'order': self.associated_order.code,
|
'order': self.orderposition.order.code,
|
||||||
'secret': self.associated_order.secret,
|
'secret': self.orderposition.order.secret,
|
||||||
'answer': self.pk,
|
'answer': self.pk,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
url = eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
|
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={
|
||||||
'answer': self.pk,
|
'answer': self.pk,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1376,24 +1371,6 @@ class QuestionAnswer(models.Model):
|
|||||||
def file_name(self):
|
def file_name(self):
|
||||||
return self.file.name.split('.', 1)[-1]
|
return self.file.name.split('.', 1)[-1]
|
||||||
|
|
||||||
@property
|
|
||||||
def associated_order(self):
|
|
||||||
if self.orderposition:
|
|
||||||
return self.orderposition.order
|
|
||||||
elif self.order:
|
|
||||||
return self.order
|
|
||||||
|
|
||||||
@property
|
|
||||||
def event(self):
|
|
||||||
if self.orderposition:
|
|
||||||
return self.orderposition.order.event
|
|
||||||
elif self.cartposition:
|
|
||||||
return self.cartposition.event
|
|
||||||
elif self.order:
|
|
||||||
return self.order.event
|
|
||||||
elif self.checkoutsession:
|
|
||||||
return self.checkoutsession.event
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_string(use_cached=True)
|
return self.to_string(use_cached=True)
|
||||||
|
|
||||||
@@ -1449,12 +1426,6 @@ class QuestionAnswer(models.Model):
|
|||||||
else:
|
else:
|
||||||
return self.answer
|
return self.answer
|
||||||
|
|
||||||
def to_dependency_values(self):
|
|
||||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
|
||||||
return [o.identifier for o in self.options.all()]
|
|
||||||
elif self.question.type in (Question.TYPE_BOOLEAN, Question.TYPE_COUNTRYCODE):
|
|
||||||
return self.answer
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.orderposition and self.cartposition:
|
if self.orderposition and self.cartposition:
|
||||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||||
@@ -1599,80 +1570,53 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
|||||||
|
|
||||||
def cache_answers(self, all=True):
|
def cache_answers(self, all=True):
|
||||||
"""
|
"""
|
||||||
Creates a new property on the object:
|
Creates two properties on the object.
|
||||||
questions: a list of Question objects, extended by an 'answer' property
|
(1) answ: a dictionary of question.id → answer string
|
||||||
|
(2) questions: a list of Question objects, extended by an 'answer' property
|
||||||
"""
|
"""
|
||||||
|
self.answ = {}
|
||||||
|
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
|
||||||
|
self.answ[a.question_id] = a
|
||||||
|
|
||||||
# We need to clone our question objects, otherwise we will override the cached
|
# We need to clone our question objects, otherwise we will override the cached
|
||||||
# answers of other items in the same cart if the question objects have been
|
# answers of other items in the same cart if the question objects have been
|
||||||
# selected via prefetch_related
|
# selected via prefetch_related
|
||||||
if not all:
|
if not all:
|
||||||
if hasattr(self.item, 'relevant_questionnaires'):
|
if hasattr(self.item, 'questions_to_ask'):
|
||||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||||
else:
|
else:
|
||||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type='PS') for qc in qq.children.all())
|
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
|
||||||
|
hidden=False))
|
||||||
else:
|
else:
|
||||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type__startswith='P') for qc in qq.children.all())
|
questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||||
|
|
||||||
qc_cache = {
|
question_cache = {
|
||||||
q.pk: q for q in children
|
q.pk: q for q in questions
|
||||||
}
|
}
|
||||||
|
|
||||||
def qc_is_visible(parentid, qvals):
|
def question_is_visible(parentid, qvals):
|
||||||
if parentid not in qc_cache:
|
if parentid not in question_cache:
|
||||||
return False
|
return False
|
||||||
parentqc = qc_cache[parentid]
|
parentq = question_cache[parentid]
|
||||||
if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||||
return False
|
return False
|
||||||
answer_values = self.get_dependency_answer_values(parentqc)
|
if parentid not in self.answ:
|
||||||
return any(qval in answer_values for qval in qvals)
|
return False
|
||||||
|
return (
|
||||||
|
('True' in qvals and self.answ[parentid].answer == 'True')
|
||||||
|
or ('False' in qvals and self.answ[parentid].answer == 'False')
|
||||||
|
or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals))
|
||||||
|
)
|
||||||
|
|
||||||
self.questions = []
|
self.questions = []
|
||||||
for qc in children:
|
for q in questions:
|
||||||
if qc.user_datafield_id and qc.user_datafield_id in self.answer_cache:
|
if q.id in self.answ:
|
||||||
qc.answer = self.answer_cache[qc.user_datafield_id]
|
q.answer = self.answ[q.id]
|
||||||
#qc.answer.question = qc # cache object
|
q.answer.question = q # cache object
|
||||||
elif qc.system_datafield:
|
|
||||||
qc.answer = self.get_system_answer(qc.system_datafield)
|
|
||||||
#qc.answer.question = qc # cache object
|
|
||||||
else:
|
else:
|
||||||
qc.answer = ""
|
q.answer = ""
|
||||||
if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
|
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values):
|
||||||
self.questions.append(qc)
|
self.questions.append(q)
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def answer_cache(self):
|
|
||||||
return {
|
|
||||||
aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_dependency_answer_values(self, qc):
|
|
||||||
if qc.user_datafield_id:
|
|
||||||
if qc.user_datafield_id not in self.answer_cache:
|
|
||||||
return None
|
|
||||||
answer = self.answer_cache[qc.user_datafield_id]
|
|
||||||
return answer.to_dependency_values()
|
|
||||||
elif qc.system_datafield:
|
|
||||||
return [self.get_system_answer(qc.system_datafield)]
|
|
||||||
else:
|
|
||||||
raise ValueError('Questionnaire child without datafield has no answer')
|
|
||||||
|
|
||||||
def get_system_answer(self, system_datafield_name):
|
|
||||||
if system_datafield_name == 'attendee_name_parts':
|
|
||||||
return self.attendee_name_parts
|
|
||||||
elif system_datafield_name == 'attendee_email':
|
|
||||||
return self.attendee_email
|
|
||||||
elif system_datafield_name == 'street':
|
|
||||||
return self.street
|
|
||||||
elif system_datafield_name == 'zipcode':
|
|
||||||
return self.zipcode
|
|
||||||
elif system_datafield_name == 'city':
|
|
||||||
return self.city
|
|
||||||
elif system_datafield_name == 'state':
|
|
||||||
return self.state
|
|
||||||
elif system_datafield_name == 'country':
|
|
||||||
return self.country
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown system question name')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def net_price(self):
|
def net_price(self):
|
||||||
@@ -1731,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else ''
|
self.country.name
|
||||||
]
|
]
|
||||||
lines = [r.strip() for r in lines if r]
|
lines = [r.strip() for r in lines if r]
|
||||||
return '\n'.join(lines).strip()
|
return '\n'.join(lines).strip()
|
||||||
@@ -2107,17 +2051,6 @@ class OrderPayment(models.Model):
|
|||||||
"""
|
"""
|
||||||
return '{}-P-{}'.format(self.order.code, self.local_id)
|
return '{}-P-{}'.format(self.order.code, self.local_id)
|
||||||
|
|
||||||
@property
|
|
||||||
def global_id(self):
|
|
||||||
"""
|
|
||||||
The global ID of this payment, constructed by the organizer slug, event slug, and the full id.
|
|
||||||
"""
|
|
||||||
return "{organizer}-{event}-{full_id}".format(
|
|
||||||
organizer=self.order.organizer.slug.upper(),
|
|
||||||
event=self.order.event.slug.upper(),
|
|
||||||
full_id=self.full_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.local_id:
|
if not self.local_id:
|
||||||
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||||
@@ -2318,17 +2251,6 @@ class OrderRefund(models.Model):
|
|||||||
"""
|
"""
|
||||||
return '{}-R-{}'.format(self.order.code, self.local_id)
|
return '{}-R-{}'.format(self.order.code, self.local_id)
|
||||||
|
|
||||||
@property
|
|
||||||
def global_id(self):
|
|
||||||
"""
|
|
||||||
The global ID of this refund, constructed by the organizer slug, event slug, and the full id.
|
|
||||||
"""
|
|
||||||
return "{organizer}-{event}-{full_id}".format(
|
|
||||||
organizer=self.order.organizer.slug.upper(),
|
|
||||||
event=self.order.event.slug.upper(),
|
|
||||||
full_id=self.full_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.local_id:
|
if not self.local_id:
|
||||||
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||||
@@ -2343,12 +2265,9 @@ class OrderRefund(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def ActivePositionManager(**scope):
|
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
||||||
class InnerClass(ScopedManager(**scope).__class__):
|
def get_queryset(self):
|
||||||
def get_queryset(self):
|
return super().get_queryset().filter(canceled=False)
|
||||||
return super().get_queryset().filter(canceled=False)
|
|
||||||
|
|
||||||
return InnerClass()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderFee(RoundingCorrectionMixin, models.Model):
|
class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||||
@@ -2415,8 +2334,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
|||||||
)
|
)
|
||||||
description = models.CharField(max_length=190, blank=True)
|
description = models.CharField(max_length=190, blank=True)
|
||||||
internal_type = models.CharField(max_length=255, blank=True)
|
internal_type = models.CharField(max_length=255, blank=True)
|
||||||
tax_rate = NormalizedDecimalField(
|
tax_rate = models.DecimalField(
|
||||||
max_digits=7, decimal_places=4,
|
max_digits=7, decimal_places=2,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -2438,7 +2357,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
|||||||
canceled = models.BooleanField(default=False)
|
canceled = models.BooleanField(default=False)
|
||||||
|
|
||||||
all = ScopedManager(organizer='order__event__organizer')
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager(organizer='order__event__organizer')
|
objects = ActivePositionManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def net_value(self):
|
def net_value(self):
|
||||||
@@ -2600,6 +2519,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="order_positions",
|
related_name="order_positions",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
Order,
|
Order,
|
||||||
@@ -2612,8 +2533,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
tax_rate = NormalizedDecimalField(
|
tax_rate = models.DecimalField(
|
||||||
max_digits=7, decimal_places=4,
|
max_digits=7, decimal_places=2,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -2656,8 +2577,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
all = ScopedManager(organizer='organizer')
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager(organizer='organizer')
|
objects = ActivePositionManager()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -3131,8 +3052,8 @@ class Transaction(models.Model):
|
|||||||
price_includes_rounding_correction = models.DecimalField(
|
price_includes_rounding_correction = models.DecimalField(
|
||||||
max_digits=13, decimal_places=2, default=Decimal("0.00")
|
max_digits=13, decimal_places=2, default=Decimal("0.00")
|
||||||
)
|
)
|
||||||
tax_rate = NormalizedDecimalField(
|
tax_rate = models.DecimalField(
|
||||||
max_digits=7, decimal_places=4,
|
max_digits=7, decimal_places=2,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -3212,39 +3133,6 @@ class Transaction(models.Model):
|
|||||||
return self.tax_value_includes_rounding_correction * self.count
|
return self.tax_value_includes_rounding_correction * self.count
|
||||||
|
|
||||||
|
|
||||||
class CheckoutSession(models.Model):
|
|
||||||
"""
|
|
||||||
A checkout session optionally bundles cart positions with additional information. This is historically
|
|
||||||
not required in pretix and currently only used in the Storefront API.
|
|
||||||
"""
|
|
||||||
event = models.ForeignKey(
|
|
||||||
Event,
|
|
||||||
verbose_name=_("Event"),
|
|
||||||
related_name="checkout_sessions",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
cart_id = models.CharField(
|
|
||||||
max_length=255, unique=True,
|
|
||||||
verbose_name=_("Cart ID (e.g. session key)"),
|
|
||||||
)
|
|
||||||
created = models.DateTimeField(
|
|
||||||
verbose_name=_("Date"),
|
|
||||||
auto_now_add=True,
|
|
||||||
)
|
|
||||||
customer = models.ForeignKey(
|
|
||||||
Customer,
|
|
||||||
related_name='checkout_sessions',
|
|
||||||
null=True, blank=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
sales_channel = models.ForeignKey(
|
|
||||||
"SalesChannel",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
testmode = models.BooleanField(default=False)
|
|
||||||
session_data = models.JSONField(default=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class CartPosition(AbstractPosition):
|
class CartPosition(AbstractPosition):
|
||||||
"""
|
"""
|
||||||
A cart position is similar to an order line, except that it is not
|
A cart position is similar to an order line, except that it is not
|
||||||
@@ -3280,8 +3168,8 @@ class CartPosition(AbstractPosition):
|
|||||||
verbose_name=_("Limit for extending expiration date"),
|
verbose_name=_("Limit for extending expiration date"),
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
tax_rate = NormalizedDecimalField(
|
tax_rate = models.DecimalField(
|
||||||
max_digits=7, decimal_places=4, default=Decimal('0'),
|
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_code = models.CharField(
|
tax_code = models.CharField(
|
||||||
@@ -3449,13 +3337,6 @@ class CartPosition(AbstractPosition):
|
|||||||
|
|
||||||
class InvoiceAddress(models.Model):
|
class InvoiceAddress(models.Model):
|
||||||
last_modified = models.DateTimeField(auto_now=True)
|
last_modified = models.DateTimeField(auto_now=True)
|
||||||
checkout_session = models.OneToOneField(
|
|
||||||
CheckoutSession,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name='invoice_address',
|
|
||||||
on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
|
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
|
||||||
customer = models.ForeignKey(
|
customer = models.ForeignKey(
|
||||||
Customer,
|
Customer,
|
||||||
@@ -3535,7 +3416,7 @@ class InvoiceAddress(models.Model):
|
|||||||
self.name,
|
self.name,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else '',
|
self.country.name,
|
||||||
self.vat_id,
|
self.vat_id,
|
||||||
self.custom_field,
|
self.custom_field,
|
||||||
self.internal_reference,
|
self.internal_reference,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import operator
|
|||||||
import string
|
import string
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import pytz_deprecation_shim
|
import pytz_deprecation_shim
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -62,9 +61,6 @@ from ...helpers.permission_migration import (
|
|||||||
from ..settings import settings_hierarkey
|
from ..settings import settings_hierarkey
|
||||||
from .auth import User
|
from .auth import User
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from hierarkey.proxy import HierarkeyProxy
|
|
||||||
|
|
||||||
|
|
||||||
@settings_hierarkey.add(cache_namespace='organizer')
|
@settings_hierarkey.add(cache_namespace='organizer')
|
||||||
class Organizer(LoggedModel):
|
class Organizer(LoggedModel):
|
||||||
@@ -82,9 +78,6 @@ class Organizer(LoggedModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
settings_namespace = 'organizer'
|
settings_namespace = 'organizer'
|
||||||
if TYPE_CHECKING:
|
|
||||||
settings: HierarkeyProxy
|
|
||||||
|
|
||||||
name = models.CharField(max_length=200,
|
name = models.CharField(max_length=200,
|
||||||
verbose_name=_("Name"))
|
verbose_name=_("Name"))
|
||||||
slug = models.CharField(
|
slug = models.CharField(
|
||||||
|
|||||||
@@ -118,10 +118,7 @@ class SeatingPlan(LoggedModel):
|
|||||||
for zi, z in enumerate(self.layout_data['zones']):
|
for zi, z in enumerate(self.layout_data['zones']):
|
||||||
zpos = (z['position']['x'], z['position']['y'])
|
zpos = (z['position']['x'], z['position']['y'])
|
||||||
for ri, r in enumerate(z['rows']):
|
for ri, r in enumerate(z['rows']):
|
||||||
rpos = (
|
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
|
||||||
zpos[0] + r.get('position', {}).get('x', 0),
|
|
||||||
zpos[1] + r.get('position', {}).get('y', 0),
|
|
||||||
)
|
|
||||||
row_label = None
|
row_label = None
|
||||||
if r.get('row_label'):
|
if r.get('row_label'):
|
||||||
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
||||||
@@ -150,8 +147,8 @@ class SeatingPlan(LoggedModel):
|
|||||||
zone=z['name'],
|
zone=z['name'],
|
||||||
category=s['category'],
|
category=s['category'],
|
||||||
sorting_rank=rank,
|
sorting_rank=rank,
|
||||||
x=rpos[0] + s.get('position', {}).get('x', 0),
|
x=rpos[0] + s['position']['x'],
|
||||||
y=rpos[1] + s.get('position', {}).get('y', 0),
|
y=rpos[1] + s['position']['y'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from pretix.base.decimal import round_decimal
|
|||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
from pretix.helpers.models import NormalizedDecimalField
|
|
||||||
|
|
||||||
|
|
||||||
class TaxedPrice:
|
class TaxedPrice:
|
||||||
@@ -336,9 +335,9 @@ class TaxRule(LoggedModel):
|
|||||||
max_length=190,
|
max_length=190,
|
||||||
choices=TAX_CODE_LISTS,
|
choices=TAX_CODE_LISTS,
|
||||||
)
|
)
|
||||||
rate = NormalizedDecimalField(
|
rate = models.DecimalField(
|
||||||
max_digits=7,
|
max_digits=10,
|
||||||
decimal_places=4,
|
decimal_places=2,
|
||||||
validators=[
|
validators=[
|
||||||
MaxValueValidator(
|
MaxValueValidator(
|
||||||
limit_value=Decimal("100.00"),
|
limit_value=Decimal("100.00"),
|
||||||
|
|||||||
@@ -32,10 +32,8 @@
|
|||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
import datetime
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from decimal import ROUND_HALF_UP, Decimal
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -423,33 +421,27 @@ class Voucher(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_affected_quotas(quota, item, variation, subevent):
|
def clean_quota_get_ignored(old_instance):
|
||||||
if quota:
|
quotas = set()
|
||||||
return {quota}
|
was_valid = old_instance and (
|
||||||
elif item and variation:
|
old_instance.valid_until is None or old_instance.valid_until >= now()
|
||||||
return set(variation.quotas.filter(subevent=subevent))
|
)
|
||||||
elif item and not item.has_variations:
|
if old_instance and old_instance.block_quota and was_valid:
|
||||||
return set(item.quotas.filter(subevent=subevent))
|
if old_instance.quota:
|
||||||
elif item and item.has_variations:
|
quotas.add(old_instance.quota)
|
||||||
return set(
|
elif old_instance.variation:
|
||||||
Quota.objects.filter(
|
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||||
pk__in=Quota.variations.through.objects.filter(
|
elif old_instance.item:
|
||||||
itemvariation__item=item,
|
if old_instance.item.has_variations:
|
||||||
quota__subevent=subevent,
|
quotas |= set(
|
||||||
).values('quota_id')
|
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
||||||
)
|
itemvariation__item=old_instance.item,
|
||||||
)
|
quota__subevent=old_instance.subevent,
|
||||||
else:
|
).values('quota_id'))
|
||||||
return set()
|
)
|
||||||
|
else:
|
||||||
@staticmethod
|
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||||
def clean_quota_get_ignored(voucher_data: Union["VoucherBulkData", "Voucher"]):
|
return quotas
|
||||||
if voucher_data:
|
|
||||||
valid = voucher_data.valid_until is None or voucher_data.valid_until >= now()
|
|
||||||
if valid and voucher_data.block_quota and voucher_data.max_usages > voucher_data.redeemed:
|
|
||||||
return Voucher.get_affected_quotas(voucher_data.quota, voucher_data.item, voucher_data.variation, voucher_data.subevent)
|
|
||||||
|
|
||||||
return set()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||||
@@ -461,8 +453,22 @@ class Voucher(LoggedModel):
|
|||||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||||
|
|
||||||
new_quotas = Voucher.get_affected_quotas(quota, item, variation, data.get('subevent'))
|
if quota:
|
||||||
if not new_quotas:
|
new_quotas = {quota}
|
||||||
|
elif item and variation:
|
||||||
|
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||||
|
elif item and not item.has_variations:
|
||||||
|
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||||
|
elif item and item.has_variations:
|
||||||
|
new_quotas = set(
|
||||||
|
Quota.objects.filter(
|
||||||
|
pk__in=Quota.variations.through.objects.filter(
|
||||||
|
itemvariation__item=item,
|
||||||
|
quota__subevent=data.get('subevent'),
|
||||||
|
).values('quota_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|
||||||
@@ -638,16 +644,3 @@ class Voucher(LoggedModel):
|
|||||||
]
|
]
|
||||||
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
||||||
return ops
|
return ops
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VoucherBulkData:
|
|
||||||
item: object
|
|
||||||
variation: object
|
|
||||||
quota: object
|
|
||||||
block_quota: bool
|
|
||||||
valid_until: datetime.datetime
|
|
||||||
subevent: object
|
|
||||||
redeemed: int
|
|
||||||
max_usages: int
|
|
||||||
allow_ignore_quota: bool
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from pretix.base.models import Event, LogEntry
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.base.signals import register_notification_types
|
from pretix.base.signals import register_notification_types
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_TYPES = None
|
_ALL_TYPES = None
|
||||||
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
|||||||
def build_notification(self, logentry: LogEntry):
|
def build_notification(self, logentry: LogEntry):
|
||||||
order = logentry.content_object
|
order = logentry.content_object
|
||||||
|
|
||||||
order_url = mainreverse_absolute(
|
order_url = build_absolute_uri(
|
||||||
'control:event.order',
|
'control:event.order',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': logentry.event.organizer.slug,
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
|||||||
+12
-81
@@ -71,8 +71,8 @@ from pretix.helpers import OF_SELF
|
|||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
from pretix.helpers.money import DecimalTextInput
|
from pretix.helpers.money import DecimalTextInput
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.views import get_cart_positions
|
from pretix.presale.views import get_cart
|
||||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -330,24 +330,9 @@ class BasePaymentProvider:
|
|||||||
payment method. This returns ``False`` by default which is no guarantee that
|
payment method. This returns ``False`` by default which is no guarantee that
|
||||||
aborting a pending payment can never happen, it just hides the frontend button
|
aborting a pending payment can never happen, it just hides the frontend button
|
||||||
to avoid users accidentally committing double payments.
|
to avoid users accidentally committing double payments.
|
||||||
If the decision doesn't depend on the specific payment, then only implementing
|
|
||||||
``abort_pending_allowed`` is enough, ``payment_abort_pending_allowed(payment: OrderPayment)``
|
|
||||||
is expected to take this into account.
|
|
||||||
As a consumer only evaluate ``payment_abort_pending_allowed(payment: OrderPayment)``
|
|
||||||
to check if aborting this pending payment is possible.
|
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _payment_abort_pending_allowed(self, payment: OrderPayment) -> bool:
|
|
||||||
"""
|
|
||||||
Experimental: This might change during upcomming releases.
|
|
||||||
Whether or not a user can abort a payment in pending state to switch to another
|
|
||||||
payment method. This returns ``self.abort_pending_allowed`` by default which is
|
|
||||||
no guarantee that aborting a pending payment can never happen, it just hides the
|
|
||||||
frontend button to avoid users accidentally committing double payments.
|
|
||||||
"""
|
|
||||||
return self.abort_pending_allowed
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requires_invoice_immediately(self):
|
def requires_invoice_immediately(self):
|
||||||
"""
|
"""
|
||||||
@@ -394,7 +379,7 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
if not self.settings.get('_hidden_seed'):
|
if not self.settings.get('_hidden_seed'):
|
||||||
self.settings.set('_hidden_seed', get_random_string(64))
|
self.settings.set('_hidden_seed', get_random_string(64))
|
||||||
hidden_url = eventreverse_absolute(self.event, 'presale:event.payment.unlock', kwargs={
|
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={
|
||||||
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -849,7 +834,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
|
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||||
"""
|
"""
|
||||||
After the user has confirmed their purchase, this method will be called to complete
|
After the user has confirmed their purchase, this method will be called to complete
|
||||||
the payment process. This is the place to actually move the money if applicable.
|
the payment process. This is the place to actually move the money if applicable.
|
||||||
@@ -951,7 +936,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a payment.
|
Will be called if the *event administrator* views the details of a payment.
|
||||||
|
|
||||||
It should return a SafeString containing HTML code, with information regarding the current payment
|
It should return HTML code containing information regarding the current payment
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
@@ -976,7 +961,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a refund.
|
Will be called if the *event administrator* views the details of a refund.
|
||||||
|
|
||||||
It should return a SafeString containing HTML code, with information regarding the current refund
|
It should return HTML code containing information regarding the current refund
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
@@ -1034,12 +1019,10 @@ class BasePaymentProvider:
|
|||||||
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
|
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
|
||||||
On failure, you should raise a PaymentException.
|
On failure, you should raise a PaymentException.
|
||||||
"""
|
"""
|
||||||
|
if payment.state == OrderPayment.PAYMENT_STATE_PENDING and not self.abort_pending_allowed:
|
||||||
if payment.state == OrderPayment.PAYMENT_STATE_PENDING:
|
raise PaymentException(_(
|
||||||
if not self._payment_abort_pending_allowed(payment):
|
"This payment is already being processed and can not be canceled any more."
|
||||||
raise PaymentException(_(
|
))
|
||||||
"This payment is already being processed and cannot be canceled any more."
|
|
||||||
))
|
|
||||||
|
|
||||||
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||||
payment.save(update_fields=['state'])
|
payment.save(update_fields=['state'])
|
||||||
@@ -1165,7 +1148,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
|||||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||||
from .services.cart import get_fees
|
from .services.cart import get_fees
|
||||||
|
|
||||||
cart = get_cart_positions(request)
|
cart = get_cart(request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fees = get_fees(event=request.event, request=request,
|
fees = get_fees(event=request.event, request=request,
|
||||||
@@ -1433,7 +1416,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
for p in cs.get('payments', [])
|
for p in cs.get('payments', [])
|
||||||
if p.get('info_data', {}).get('gift_card')
|
if p.get('info_data', {}).get('gift_card')
|
||||||
]
|
]
|
||||||
positions = get_cart_positions(request)
|
positions = get_cart(request)
|
||||||
testmode = self.event.testmode
|
testmode = self.event.testmode
|
||||||
else:
|
else:
|
||||||
used_cards = []
|
used_cards = []
|
||||||
@@ -1723,58 +1706,6 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseHistoricalPaymentProvider(BasePaymentProvider):
|
|
||||||
"""
|
|
||||||
Base class for payment providers that no longer exist but can't be deleted to make sure historical
|
|
||||||
payments are shown correctly.
|
|
||||||
|
|
||||||
Subclasses are recommended to only implement:
|
|
||||||
- identifier
|
|
||||||
- verbose_name
|
|
||||||
- public_name
|
|
||||||
- payment_control_render
|
|
||||||
- payment_control_render_short
|
|
||||||
- refund_control_render
|
|
||||||
- refund_control_render_short
|
|
||||||
- render_invoice_text
|
|
||||||
- render_invoice_stamp
|
|
||||||
- api_payment_details
|
|
||||||
- api_refund_details
|
|
||||||
- shred_payment_info
|
|
||||||
- matching_id
|
|
||||||
- refund_matching_id
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_enabled(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def settings_form_fields(self) -> dict:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
|
||||||
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
|
||||||
|
|
||||||
def execute_refund(self, refund: OrderRefund):
|
|
||||||
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||||
def register_payment_provider(sender, **kwargs):
|
def register_payment_provider(sender, **kwargs):
|
||||||
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
|
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
|
||||||
|
|||||||
+31
-32
@@ -77,7 +77,6 @@ from reportlab.platypus import Paragraph
|
|||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
|
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
|
||||||
from pretix.base.services.placeholders import PlaceholderContext
|
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import layout_image_variables, layout_text_variables
|
from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
@@ -373,11 +372,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Atlantis"),
|
"editor_sample": _("Atlantis"),
|
||||||
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
|
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("invoice_custom_field", {
|
|
||||||
"label": _("Invoice custom recipient field"),
|
|
||||||
"editor_sample": _("Custom recipient field"),
|
|
||||||
"evaluate": lambda op, order, ev: order.invoice_address.custom_field if getattr(order, 'invoice_address', None) else ''
|
|
||||||
}),
|
|
||||||
("addons", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
||||||
@@ -402,7 +396,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Event organizer info text"),
|
"editor_sample": _("Event organizer info text"),
|
||||||
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
|
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
|
||||||
}),
|
}),
|
||||||
("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
|
("event_info_text", {
|
||||||
|
"label": _("Event info text"),
|
||||||
|
"editor_sample": _("Event info text"),
|
||||||
|
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
|
||||||
|
}),
|
||||||
("now_date", {
|
("now_date", {
|
||||||
"label": _("Printing date"),
|
"label": _("Printing date"),
|
||||||
"editor_sample": _("2017-05-31"),
|
"editor_sample": _("2017-05-31"),
|
||||||
@@ -667,14 +665,6 @@ def get_images(event):
|
|||||||
def get_variables(event):
|
def get_variables(event):
|
||||||
v = copy.copy(DEFAULT_VARIABLES)
|
v = copy.copy(DEFAULT_VARIABLES)
|
||||||
|
|
||||||
templating_context = PlaceholderContext(event=event)
|
|
||||||
v['event_info_text'] = {
|
|
||||||
"label": _("Event info text"),
|
|
||||||
"editor_sample": _("Event info text"),
|
|
||||||
"evaluate": lambda op, order, ev:
|
|
||||||
templating_context.format(str(order.event.settings.event_info_text))
|
|
||||||
}
|
|
||||||
|
|
||||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||||
|
|
||||||
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
|
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
|
||||||
@@ -1072,7 +1062,7 @@ class Renderer:
|
|||||||
except:
|
except:
|
||||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
|
p = Paragraph(text, style=style)
|
||||||
return p, ad, lineheight
|
return p, ad, lineheight
|
||||||
|
|
||||||
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
@@ -1232,7 +1222,9 @@ class Renderer:
|
|||||||
|
|
||||||
for i, page in enumerate(fg_pdf.pages):
|
for i, page in enumerate(fg_pdf.pages):
|
||||||
bg_page = self.bg_pdf.pages[i]
|
bg_page = self.bg_pdf.pages[i]
|
||||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
_correct_page_media_box(bg_page)
|
||||||
|
page.merge_page(bg_page, over=False)
|
||||||
|
output.add_page(page)
|
||||||
|
|
||||||
# pdf_header is a string like "%pdf-X.X"
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
@@ -1297,32 +1289,39 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
|||||||
bg_pdf.write(bg_filename)
|
bg_pdf.write(bg_filename)
|
||||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||||
else:
|
else:
|
||||||
output = PdfWriter()
|
|
||||||
for i, page in enumerate(fg_pdf.pages):
|
for i, page in enumerate(fg_pdf.pages):
|
||||||
bg_page = bg_pdf.pages[i]
|
bg_page = bg_pdf.pages[i]
|
||||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
_correct_page_media_box(bg_page)
|
||||||
|
page.merge_page(bg_page, over=False)
|
||||||
|
|
||||||
# pdf_header is a string like "%pdf-X.X"
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
output.pdf_header = (
|
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
bg_pdf.pdf_header
|
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
|
|
||||||
else fg_pdf.pdf_header
|
fg_pdf.write(out_file)
|
||||||
)
|
|
||||||
output.write(out_file)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
|
def _correct_page_media_box(page: pypdf.PageObject):
|
||||||
if bg_page.rotation != 0:
|
if page.rotation != 0:
|
||||||
bg_page.transfer_rotation_to_content()
|
page.transfer_rotation_to_content()
|
||||||
media_box = bg_page.mediabox
|
media_box = page.mediabox
|
||||||
trsf = pypdf.Transformation()
|
trsf = pypdf.Transformation()
|
||||||
if media_box.bottom != 0:
|
if media_box.bottom != 0:
|
||||||
trsf = trsf.translate(0, -media_box.bottom)
|
trsf = trsf.translate(0, -media_box.bottom)
|
||||||
if media_box.left != 0:
|
if media_box.left != 0:
|
||||||
trsf = trsf.translate(-media_box.left, 0)
|
trsf = trsf.translate(-media_box.left, 0)
|
||||||
|
page.add_transformation(trsf, False)
|
||||||
fg_page = output.add_page(fg_page)
|
for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
|
||||||
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
|
if b in page:
|
||||||
|
rr = pypdf.generic.RectangleObject(page[b])
|
||||||
|
pt1 = trsf.apply_on(rr.lower_left)
|
||||||
|
pt2 = trsf.apply_on(rr.upper_right)
|
||||||
|
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
|
||||||
|
min(pt1[0], pt2[0]),
|
||||||
|
min(pt1[1], pt2[1]),
|
||||||
|
max(pt1[0], pt2[0]),
|
||||||
|
max(pt1[1], pt2[1]),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
|
|||||||
+155
-373
@@ -19,245 +19,30 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
|
||||||
import warnings
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from dataclasses import dataclass
|
from typing import Union
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING, Iterable, List, Literal, Optional, Tuple, Union, cast,
|
|
||||||
)
|
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms.widgets import Widget
|
|
||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import Promise, lazy
|
from django.utils.functional import lazy
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from pretix.base.forms.widgets import OptionAttrsSelect
|
BASE_CHOICES = (
|
||||||
|
('date_from', _('Event start')),
|
||||||
|
('date_to', _('Event end')),
|
||||||
|
('date_admission', _('Event admission')),
|
||||||
|
('presale_start', _('Presale start')),
|
||||||
|
('presale_end', _('Presale end')),
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from'))
|
||||||
from .models import Event, Order, SubEvent
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BaseChoice:
|
|
||||||
base: Literal["event", "order"]
|
|
||||||
attribute: str
|
|
||||||
text: Promise
|
|
||||||
supports_before: bool
|
|
||||||
supports_after: bool
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key(self) -> str:
|
|
||||||
key = f"{self.base}__{self.attribute}"
|
|
||||||
return key
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find(objects: Iterable["BaseChoice"], key: str) -> "BaseChoice":
|
|
||||||
if "__" in key:
|
|
||||||
choice = next((obj for obj in objects if obj.key == key), None)
|
|
||||||
else:
|
|
||||||
# fallback for RelativeDateFields stored, before support for bases other than event was added
|
|
||||||
choice = next((obj for obj in objects if obj.attribute == key and obj.base == "event"), None)
|
|
||||||
|
|
||||||
if choice is None:
|
|
||||||
raise TypeError(f"key {key} must be a valid key in BASE_CHOICES")
|
|
||||||
|
|
||||||
return choice
|
|
||||||
|
|
||||||
|
|
||||||
BASE_CHOICES: List[BaseChoice] = [
|
|
||||||
BaseChoice('event', 'date_from', _('Event start'), True, True),
|
|
||||||
BaseChoice('event', 'date_to', _('Event end'), True, True),
|
|
||||||
BaseChoice('event', 'date_admission', _('Event admission'), True, True),
|
|
||||||
BaseChoice('event', 'presale_start', _('Presale start'), True, True),
|
|
||||||
BaseChoice('event', 'presale_end', _('Presale end'), True, True),
|
|
||||||
BaseChoice('order', 'datetime', _('Order creation'), False, True),
|
|
||||||
BaseChoice('order', 'expires', _('Order expiry'), True, True),
|
|
||||||
]
|
|
||||||
|
|
||||||
EVENT_BASE_CHOICES = [
|
|
||||||
x for x in BASE_CHOICES if x.base == 'event'
|
|
||||||
]
|
|
||||||
|
|
||||||
ORDER_BASE_CHOICES = [
|
|
||||||
x for x in BASE_CHOICES if x.base == 'order'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class RelativeDate:
|
|
||||||
"""
|
|
||||||
This contains information on a date that is defined in relation to a fixed base point.
|
|
||||||
This means that the underlying data is a fixed date as the base point and a number of days or a time interval
|
|
||||||
to calculate the date.
|
|
||||||
|
|
||||||
The list of valid base date choices is defined in BASE_CHOICES.
|
|
||||||
If the base_date_name is not set, the date_from attribute of Event is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
days: int = 0
|
|
||||||
minutes: Optional[int] = None
|
|
||||||
time: Optional[datetime.time] = None
|
|
||||||
is_after: bool = False
|
|
||||||
base_date_name: str = 'event__date_from__'
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if self.is_after and not self._choice.supports_after:
|
|
||||||
raise ValueError(
|
|
||||||
"The selected base date and attribute combination does not support relative dates placed after the base date"
|
|
||||||
)
|
|
||||||
if not self.is_after and not self._choice.supports_before:
|
|
||||||
raise ValueError(
|
|
||||||
"The selected base date and attribute combination does not support relative dates placed before the base date")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _choice(self):
|
|
||||||
return BaseChoice.find(BASE_CHOICES, self.base_date_name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key(self):
|
|
||||||
return self._choice.key
|
|
||||||
|
|
||||||
def __eq__(self, o: object) -> bool:
|
|
||||||
if not isinstance(o, RelativeDate):
|
|
||||||
return False
|
|
||||||
return self.to_string() == o.to_string()
|
|
||||||
|
|
||||||
def _resolve_date(self, base: "Event | Order | SubEvent") -> Tuple[datetime.datetime, ZoneInfo]:
|
|
||||||
"""
|
|
||||||
Resolves the datetime and timezone information of this RelativeDate object in relation to the provided reference.
|
|
||||||
|
|
||||||
:param base: the reference which should be used to resolve the relative date
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
from .models import Event, Order, SubEvent
|
|
||||||
|
|
||||||
choice = self._choice
|
|
||||||
|
|
||||||
if choice.base == "order" and isinstance(base, Order):
|
|
||||||
event = base.event
|
|
||||||
base_date = getattr(base, choice.attribute)
|
|
||||||
elif choice.base == "event" and isinstance(base, SubEvent):
|
|
||||||
event = base.event
|
|
||||||
base_date = (getattr(base, choice.attribute) or
|
|
||||||
getattr(base.event, choice.attribute) or
|
|
||||||
base.date_from)
|
|
||||||
elif choice.base == "event" and isinstance(base, Event):
|
|
||||||
event = base
|
|
||||||
base_date = getattr(base, choice.attribute) or event.date_from
|
|
||||||
else:
|
|
||||||
raise TypeError("The base defined by data does not match the passed in base")
|
|
||||||
|
|
||||||
tz = ZoneInfo(event.settings.timezone)
|
|
||||||
return base_date, tz
|
|
||||||
|
|
||||||
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
|
|
||||||
"""
|
|
||||||
Resolves the effective date of this RelativeDate object in relation to the provided reference.
|
|
||||||
|
|
||||||
:param base: the reference which should be used to resolve the date
|
|
||||||
:return: datetime.date
|
|
||||||
"""
|
|
||||||
if self.minutes is not None:
|
|
||||||
raise ValueError('A minute-based relative datetime can not be used as a date')
|
|
||||||
|
|
||||||
base_date, tz = self._resolve_date(base)
|
|
||||||
|
|
||||||
if self.is_after:
|
|
||||||
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.days)
|
|
||||||
else:
|
|
||||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.days)
|
|
||||||
return new_date.date()
|
|
||||||
|
|
||||||
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
|
|
||||||
"""
|
|
||||||
Resolves the effective datetime of this RelativeDate object in relation to the provided reference.
|
|
||||||
|
|
||||||
:param base: the reference which should be used to resolve the datetime
|
|
||||||
:return: datetime.datetime
|
|
||||||
"""
|
|
||||||
base_date, tz = self._resolve_date(base)
|
|
||||||
|
|
||||||
if self.minutes is not None:
|
|
||||||
if self.is_after:
|
|
||||||
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.minutes)
|
|
||||||
else:
|
|
||||||
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.minutes)
|
|
||||||
else:
|
|
||||||
if self.is_after:
|
|
||||||
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.days)).astimezone(tz)
|
|
||||||
else:
|
|
||||||
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.days)).astimezone(tz)
|
|
||||||
if self.time:
|
|
||||||
new_date = new_date.replace(
|
|
||||||
hour=self.time.hour,
|
|
||||||
minute=self.time.minute,
|
|
||||||
second=self.time.second
|
|
||||||
)
|
|
||||||
new_date = new_date.astimezone(tz)
|
|
||||||
return new_date
|
|
||||||
|
|
||||||
def to_string(self) -> str:
|
|
||||||
if self.minutes is not None:
|
|
||||||
return 'RELDATE/minutes/{}/{}/{}'.format( #
|
|
||||||
self.minutes,
|
|
||||||
self._choice.key,
|
|
||||||
'after' if self.is_after else '',
|
|
||||||
)
|
|
||||||
return 'RELDATE/{}/{}/{}/{}'.format( #
|
|
||||||
self.days,
|
|
||||||
self.time.strftime('%H:%M:%S') if self.time else '-',
|
|
||||||
self._choice.key,
|
|
||||||
'after' if self.is_after else '',
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_string(cls, input: str):
|
|
||||||
if not input.startswith('RELDATE/'):
|
|
||||||
raise TypeError("Invalid input for RelativeDate.from_string()")
|
|
||||||
|
|
||||||
parts = input.split('/')
|
|
||||||
if parts[1] == 'minutes':
|
|
||||||
data = RelativeDate(
|
|
||||||
days=0,
|
|
||||||
minutes=int(parts[2]),
|
|
||||||
base_date_name=parts[3],
|
|
||||||
time=None,
|
|
||||||
is_after=len(parts) > 4 and parts[4] == "after",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if parts[2] == '-':
|
|
||||||
time = None
|
|
||||||
else:
|
|
||||||
timeparts = parts[2].split(':')
|
|
||||||
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
|
|
||||||
try:
|
|
||||||
data = RelativeDate(
|
|
||||||
days=int(parts[1] or 0),
|
|
||||||
base_date_name=parts[3],
|
|
||||||
time=time,
|
|
||||||
minutes=None,
|
|
||||||
is_after=len(parts) > 4 and parts[4] == "after",
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
data = RelativeDate(
|
|
||||||
days=0,
|
|
||||||
base_date_name=parts[3],
|
|
||||||
time=time,
|
|
||||||
minutes=None,
|
|
||||||
is_after=len(parts) > 4 and parts[4] == "after",
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeDateWrapper:
|
class RelativeDateWrapper:
|
||||||
@@ -265,53 +50,130 @@ class RelativeDateWrapper:
|
|||||||
This contains information on a date that might be relative to an event. This means
|
This contains information on a date that might be relative to an event. This means
|
||||||
that the underlying data is either a fixed date or a number of days and a wall clock
|
that the underlying data is either a fixed date or a number of days and a wall clock
|
||||||
time to calculate the date based on a base point.
|
time to calculate the date based on a base point.
|
||||||
|
|
||||||
|
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
|
||||||
|
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
|
||||||
|
will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
|
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
|
def date(self, event) -> datetime.date:
|
||||||
"""
|
from .models import SubEvent
|
||||||
If the RelativeDateWrapper wraps a RelativeDate object:
|
|
||||||
Resolves the effective date of this object in relation to the provided reference.
|
|
||||||
If the RelativeDateWrapper wraps an absolute date or datetime:
|
|
||||||
Returns the wrapped absolute date.
|
|
||||||
|
|
||||||
:param base: the reference which should be used to resolve the date in case it is a relative date.
|
|
||||||
:return: datetime.date
|
|
||||||
"""
|
|
||||||
if isinstance(self.data, datetime.datetime):
|
if isinstance(self.data, datetime.datetime):
|
||||||
return self.data.date()
|
return self.data.date()
|
||||||
elif isinstance(self.data, datetime.date):
|
elif isinstance(self.data, datetime.date):
|
||||||
return self.data
|
return self.data
|
||||||
else:
|
else:
|
||||||
return self.data.date(base)
|
if self.data.minutes is not None:
|
||||||
|
raise ValueError('A minute-based relative datetime can not be used as a date')
|
||||||
|
|
||||||
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
|
tz = ZoneInfo(event.settings.timezone)
|
||||||
"""
|
if isinstance(event, SubEvent):
|
||||||
If the RelativeDateWrapper wraps a RelativeDate object:
|
base_date = (
|
||||||
Resolves the effective datetime of this object in relation to the provided reference.
|
getattr(event, self.data.base_date_name)
|
||||||
If the RelativeDateWrapper wraps an absolute date or datetime:
|
or getattr(event.event, self.data.base_date_name)
|
||||||
Returns the wrapped absolute datetime.
|
or event.date_from
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
|
||||||
|
if self.data.is_after:
|
||||||
|
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
|
||||||
|
else:
|
||||||
|
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
|
||||||
|
return new_date.date()
|
||||||
|
|
||||||
|
def datetime(self, event) -> datetime.datetime:
|
||||||
|
from .models import SubEvent
|
||||||
|
|
||||||
:param base: the reference which should be used to resolve the datetime in case it is a relative datetime.
|
|
||||||
:return: datetime.datetime
|
|
||||||
"""
|
|
||||||
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
||||||
return self.data
|
return self.data
|
||||||
else:
|
else:
|
||||||
return self.data.datetime(base)
|
tz = ZoneInfo(event.settings.timezone)
|
||||||
|
if isinstance(event, SubEvent):
|
||||||
|
base_date = (
|
||||||
|
getattr(event, self.data.base_date_name)
|
||||||
|
or getattr(event.event, self.data.base_date_name)
|
||||||
|
or event.date_from
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
|
||||||
|
if self.data.minutes is not None:
|
||||||
|
if self.data.is_after:
|
||||||
|
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.data.minutes)
|
||||||
|
else:
|
||||||
|
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes)
|
||||||
|
else:
|
||||||
|
if self.data.is_after:
|
||||||
|
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)).astimezone(tz)
|
||||||
|
else:
|
||||||
|
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)).astimezone(tz)
|
||||||
|
if self.data.time:
|
||||||
|
new_date = new_date.replace(
|
||||||
|
hour=self.data.time.hour,
|
||||||
|
minute=self.data.time.minute,
|
||||||
|
second=self.data.time.second
|
||||||
|
)
|
||||||
|
new_date = new_date.astimezone(tz)
|
||||||
|
return new_date
|
||||||
|
|
||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
||||||
return self.data.isoformat()
|
return self.data.isoformat()
|
||||||
else:
|
else:
|
||||||
return self.data.to_string()
|
if self.data.minutes is not None:
|
||||||
|
return 'RELDATE/minutes/{}/{}/{}'.format( #
|
||||||
|
self.data.minutes,
|
||||||
|
self.data.base_date_name,
|
||||||
|
'after' if self.data.is_after else '',
|
||||||
|
)
|
||||||
|
return 'RELDATE/{}/{}/{}/{}'.format( #
|
||||||
|
self.data.days,
|
||||||
|
self.data.time.strftime('%H:%M:%S') if self.data.time else '-',
|
||||||
|
self.data.base_date_name,
|
||||||
|
'after' if self.data.is_after else '',
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, input: str):
|
def from_string(cls, input: str):
|
||||||
if input.startswith('RELDATE/'):
|
if input.startswith('RELDATE/'):
|
||||||
data = RelativeDate.from_string(input)
|
parts = input.split('/')
|
||||||
|
if parts[1] == 'minutes':
|
||||||
|
data = RelativeDate(
|
||||||
|
days=0,
|
||||||
|
minutes=int(parts[2]),
|
||||||
|
base_date_name=parts[3],
|
||||||
|
time=None,
|
||||||
|
is_after=len(parts) > 4 and parts[4] == "after",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if parts[2] == '-':
|
||||||
|
time = None
|
||||||
|
else:
|
||||||
|
timeparts = parts[2].split(':')
|
||||||
|
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
|
||||||
|
try:
|
||||||
|
data = RelativeDate(
|
||||||
|
days=int(parts[1] or 0),
|
||||||
|
base_date_name=parts[3],
|
||||||
|
time=time,
|
||||||
|
minutes=None,
|
||||||
|
is_after=len(parts) > 4 and parts[4] == "after",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
data = RelativeDate(
|
||||||
|
days=0,
|
||||||
|
base_date_name=parts[3],
|
||||||
|
time=time,
|
||||||
|
minutes=None,
|
||||||
|
is_after=len(parts) > 4 and parts[4] == "after",
|
||||||
|
)
|
||||||
|
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
|
||||||
|
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
|
||||||
else:
|
else:
|
||||||
data = parser.parse(input)
|
data = parser.parse(input)
|
||||||
return RelativeDateWrapper(data)
|
return RelativeDateWrapper(data)
|
||||||
@@ -325,6 +187,7 @@ BEFORE_AFTER_CHOICE = (
|
|||||||
('after', _('after')),
|
('after', _('after')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
reldatetimeparts = namedtuple('reldatetimeparts', (
|
reldatetimeparts = namedtuple('reldatetimeparts', (
|
||||||
"status", # 0
|
"status", # 0
|
||||||
"absolute", # 1
|
"absolute", # 1
|
||||||
@@ -339,14 +202,6 @@ reldatetimeparts = namedtuple('reldatetimeparts', (
|
|||||||
reldatetimeparts.indizes = reldatetimeparts(*range(9))
|
reldatetimeparts.indizes = reldatetimeparts(*range(9))
|
||||||
|
|
||||||
|
|
||||||
def _get_choices(base_choices: List[BaseChoice]) -> List[Tuple[str, Promise]]:
|
|
||||||
return [(c.key, c.text) for c in base_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_choice_validation_obj(choices: List[BaseChoice]):
|
|
||||||
return {c.key: {"data-supports-before": c.supports_before, "data-supports-after": c.supports_after} for c in choices}
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeDateTimeWidget(forms.MultiWidget):
|
class RelativeDateTimeWidget(forms.MultiWidget):
|
||||||
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
||||||
parts = reldatetimeparts
|
parts = reldatetimeparts
|
||||||
@@ -354,7 +209,6 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.status_choices = kwargs.pop('status_choices')
|
self.status_choices = kwargs.pop('status_choices')
|
||||||
base_choices = kwargs.pop('base_choices')
|
base_choices = kwargs.pop('base_choices')
|
||||||
choices = _get_choices(base_choices)
|
|
||||||
|
|
||||||
def placeholder_datetime_format():
|
def placeholder_datetime_format():
|
||||||
df = get_format('DATETIME_INPUT_FORMATS')[0]
|
df = get_format('DATETIME_INPUT_FORMATS')[0]
|
||||||
@@ -366,29 +220,21 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
tf = get_format('TIME_INPUT_FORMATS')[0]
|
tf = get_format('TIME_INPUT_FORMATS')[0]
|
||||||
return datetime.time(8, 30, 0).strftime(tf)
|
return datetime.time(8, 30, 0).strftime(tf)
|
||||||
|
|
||||||
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldatetimeparts(
|
widgets = reldatetimeparts(
|
||||||
status=forms.RadioSelect(choices=self.status_choices),
|
status=forms.RadioSelect(choices=self.status_choices),
|
||||||
absolute=forms.DateTimeInput(
|
absolute=forms.DateTimeInput(
|
||||||
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
|
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
|
||||||
),
|
),
|
||||||
rel_days_number=forms.NumberInput(),
|
rel_days_number=forms.NumberInput(),
|
||||||
rel_mins_relationto=OptionAttrsSelect(
|
rel_mins_relationto=forms.Select(choices=base_choices),
|
||||||
attrs={'data-relative-choice': True},
|
|
||||||
choices=choices,
|
|
||||||
option_attrs=_get_choice_validation_obj(base_choices)
|
|
||||||
),
|
|
||||||
rel_days_timeofday=forms.TimeInput(
|
rel_days_timeofday=forms.TimeInput(
|
||||||
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
|
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
|
||||||
),
|
),
|
||||||
rel_mins_number=forms.NumberInput(),
|
rel_mins_number=forms.NumberInput(),
|
||||||
rel_days_relationto=OptionAttrsSelect(
|
rel_days_relationto=forms.Select(choices=base_choices),
|
||||||
attrs={'data-relative-choice': True},
|
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
choices=choices,
|
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
option_attrs=_get_choice_validation_obj(base_choices)
|
)
|
||||||
),
|
|
||||||
rel_mins_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
|
|
||||||
rel_days_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
|
|
||||||
)))
|
|
||||||
super().__init__(widgets=widgets, *args, **kwargs)
|
super().__init__(widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
def decompress(self, value):
|
def decompress(self, value):
|
||||||
@@ -401,10 +247,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
status="unset",
|
status="unset",
|
||||||
absolute=None,
|
absolute=None,
|
||||||
rel_days_number=1,
|
rel_days_number=1,
|
||||||
rel_mins_relationto="event__date_from",
|
rel_mins_relationto="date_from",
|
||||||
rel_days_timeofday=None,
|
rel_days_timeofday=None,
|
||||||
rel_mins_number=0,
|
rel_mins_number=0,
|
||||||
rel_days_relationto="event__date_from",
|
rel_days_relationto="date_from",
|
||||||
rel_mins_relation="before",
|
rel_mins_relation="before",
|
||||||
rel_days_relation="before"
|
rel_days_relation="before"
|
||||||
)
|
)
|
||||||
@@ -413,10 +259,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
status="absolute",
|
status="absolute",
|
||||||
absolute=value.data,
|
absolute=value.data,
|
||||||
rel_days_number=1,
|
rel_days_number=1,
|
||||||
rel_mins_relationto="event__date_from",
|
rel_mins_relationto="date_from",
|
||||||
rel_days_timeofday=None,
|
rel_days_timeofday=None,
|
||||||
rel_mins_number=0,
|
rel_mins_number=0,
|
||||||
rel_days_relationto="event__date_from",
|
rel_days_relationto="date_from",
|
||||||
rel_mins_relation="before",
|
rel_mins_relation="before",
|
||||||
rel_days_relation="before"
|
rel_days_relation="before"
|
||||||
)
|
)
|
||||||
@@ -425,10 +271,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
status="relative_minutes",
|
status="relative_minutes",
|
||||||
absolute=None,
|
absolute=None,
|
||||||
rel_days_number=None,
|
rel_days_number=None,
|
||||||
rel_mins_relationto=value.data.key,
|
rel_mins_relationto=value.data.base_date_name,
|
||||||
rel_days_timeofday=None,
|
rel_days_timeofday=None,
|
||||||
rel_mins_number=value.data.minutes,
|
rel_mins_number=value.data.minutes,
|
||||||
rel_days_relationto=value.data.key,
|
rel_days_relationto=value.data.base_date_name,
|
||||||
rel_mins_relation="after" if value.data.is_after else "before",
|
rel_mins_relation="after" if value.data.is_after else "before",
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
rel_days_relation="after" if value.data.is_after else "before"
|
||||||
)
|
)
|
||||||
@@ -436,10 +282,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
|||||||
status="relative",
|
status="relative",
|
||||||
absolute=None,
|
absolute=None,
|
||||||
rel_days_number=value.data.days,
|
rel_days_number=value.data.days,
|
||||||
rel_mins_relationto=value.data.key,
|
rel_mins_relationto=value.data.base_date_name,
|
||||||
rel_days_timeofday=value.data.time,
|
rel_days_timeofday=value.data.time,
|
||||||
rel_mins_number=0,
|
rel_mins_number=0,
|
||||||
rel_days_relationto=value.data.key,
|
rel_days_relationto=value.data.base_date_name,
|
||||||
rel_mins_relation="after" if value.data.is_after else "before",
|
rel_mins_relation="after" if value.data.is_after else "before",
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
rel_days_relation="after" if value.data.is_after else "before"
|
||||||
)
|
)
|
||||||
@@ -463,41 +309,17 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
('relative', _('Relative date:')),
|
('relative', _('Relative date:')),
|
||||||
('relative_minutes', _('Relative time:')),
|
('relative_minutes', _('Relative time:')),
|
||||||
]
|
]
|
||||||
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
|
||||||
|
|
||||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
|
||||||
if self.relative_to_order:
|
|
||||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
|
||||||
|
|
||||||
if kwargs.get('limit_choices'):
|
if kwargs.get('limit_choices'):
|
||||||
limit = kwargs.pop('limit_choices')
|
limit = kwargs.pop('limit_choices')
|
||||||
if any(["__" not in l for l in limit]):
|
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
|
||||||
_warn_skips = (str(os.path.dirname(__file__)),)
|
else:
|
||||||
|
choices = BASE_CHOICES
|
||||||
warnings.warn(
|
|
||||||
message="Please prefix limit_choices with the base the attributes refer to, for example event__date_from",
|
|
||||||
category=DeprecationWarning,
|
|
||||||
skip_file_prefixes=_warn_skips
|
|
||||||
)
|
|
||||||
|
|
||||||
possible_choices = [
|
|
||||||
c for c in possible_choices if
|
|
||||||
# new base case as we want limit_choices to be expressed as base__attribute
|
|
||||||
(c.key in limit) or
|
|
||||||
# fallback for old event based entries
|
|
||||||
# if the base is an event, then using only attribute is fine
|
|
||||||
(c.base == "event" and c.attribute in limit)
|
|
||||||
]
|
|
||||||
|
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
|
|
||||||
choices = _get_choices(possible_choices)
|
|
||||||
|
|
||||||
fields = reldatetimeparts(
|
fields = reldatetimeparts(
|
||||||
status=forms.ChoiceField(
|
status=forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
required=True,
|
required=True
|
||||||
),
|
),
|
||||||
absolute=forms.DateTimeField(
|
absolute=forms.DateTimeField(
|
||||||
required=False
|
required=False
|
||||||
@@ -507,7 +329,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
),
|
),
|
||||||
rel_mins_relationto=forms.ChoiceField(
|
rel_mins_relationto=forms.ChoiceField(
|
||||||
choices=choices,
|
choices=choices,
|
||||||
required=False,
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_timeofday=forms.TimeField(
|
rel_days_timeofday=forms.TimeField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -517,7 +339,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
),
|
),
|
||||||
rel_days_relationto=forms.ChoiceField(
|
rel_days_relationto=forms.ChoiceField(
|
||||||
choices=choices,
|
choices=choices,
|
||||||
required=False,
|
required=False
|
||||||
),
|
),
|
||||||
rel_mins_relation=forms.ChoiceField(
|
rel_mins_relation=forms.ChoiceField(
|
||||||
choices=BEFORE_AFTER_CHOICE,
|
choices=BEFORE_AFTER_CHOICE,
|
||||||
@@ -528,9 +350,8 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'widget' not in kwargs:
|
if 'widget' not in kwargs:
|
||||||
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=possible_choices)
|
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
|
||||||
kwargs.pop('max_length', 0)
|
kwargs.pop('max_length', 0)
|
||||||
kwargs.pop('empty_value', 0)
|
kwargs.pop('empty_value', 0)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -538,24 +359,21 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
def set_event(self, event):
|
||||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [
|
||||||
if self.relative_to_order:
|
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
]
|
||||||
|
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
|
||||||
possible_choices = possible_choices
|
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||||
choices = _get_choices(possible_choices)
|
]
|
||||||
|
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = choices
|
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = choices
|
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
return None
|
return None
|
||||||
data = reldatetimeparts(*data_list)
|
data = reldatetimeparts(*data_list)
|
||||||
if data.status == 'unset':
|
if data.status == 'absolute':
|
||||||
return None
|
|
||||||
elif data.status == 'absolute':
|
|
||||||
return RelativeDateWrapper(data.absolute)
|
return RelativeDateWrapper(data.absolute)
|
||||||
|
elif data.status == 'unset':
|
||||||
|
return None
|
||||||
elif data.status == 'relative_minutes':
|
elif data.status == 'relative_minutes':
|
||||||
return RelativeDateWrapper(RelativeDate(
|
return RelativeDateWrapper(RelativeDate(
|
||||||
days=0,
|
days=0,
|
||||||
@@ -586,18 +404,6 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative':
|
|
||||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
|
||||||
if data.rel_days_relation == "before" and not choice.supports_before:
|
|
||||||
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
|
|
||||||
elif data.status == 'relative' and data.rel_days_relation == "after" and not choice.supports_after:
|
|
||||||
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
|
|
||||||
elif data.status == 'relative_minutes':
|
|
||||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
|
||||||
if data.rel_days_relation == "before" and not choice.supports_before:
|
|
||||||
raise ValidationError(_('A relative time cannot be expressed as "before" for "{}"'.format(choice.text)))
|
|
||||||
elif data.rel_days_relation == "after" and not choice.supports_after:
|
|
||||||
raise ValidationError(_('A relative time cannot be expressed as "after" for "{}"'.format(choice.text)))
|
|
||||||
|
|
||||||
return super().clean(value)
|
return super().clean(value)
|
||||||
|
|
||||||
@@ -618,20 +424,15 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.status_choices = kwargs.pop('status_choices')
|
self.status_choices = kwargs.pop('status_choices')
|
||||||
self.base_choices = kwargs.pop('base_choices')
|
widgets = reldateparts(
|
||||||
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldateparts(
|
|
||||||
status=forms.RadioSelect(choices=self.status_choices),
|
status=forms.RadioSelect(choices=self.status_choices),
|
||||||
absolute=forms.DateInput(
|
absolute=forms.DateInput(
|
||||||
attrs={'class': 'datepickerfield'}
|
attrs={'class': 'datepickerfield'}
|
||||||
),
|
),
|
||||||
rel_days_number=forms.NumberInput(),
|
rel_days_number=forms.NumberInput(),
|
||||||
rel_days_relationto=OptionAttrsSelect(
|
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
|
||||||
choices=self.base_choices,
|
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
option_attrs=_get_choice_validation_obj(self.base_choices),
|
)
|
||||||
attrs={'data-relative-choice': True},
|
|
||||||
),
|
|
||||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE, attrs={'data-relation-choice': True},),
|
|
||||||
)))
|
|
||||||
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
def decompress(self, value):
|
def decompress(self, value):
|
||||||
@@ -659,7 +460,7 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
|||||||
status="relative",
|
status="relative",
|
||||||
absolute=None,
|
absolute=None,
|
||||||
rel_days_number=value.data.days,
|
rel_days_number=value.data.days,
|
||||||
rel_days_relationto=value.data.key,
|
rel_days_relationto=value.data.base_date_name,
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
rel_days_relation="after" if value.data.is_after else "before"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -673,15 +474,6 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
]
|
]
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
|
|
||||||
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
|
||||||
|
|
||||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
|
||||||
if self.relative_to_order:
|
|
||||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
|
||||||
|
|
||||||
choices = _get_choices(possible_choices)
|
|
||||||
|
|
||||||
fields = reldateparts(
|
fields = reldateparts(
|
||||||
status=forms.ChoiceField(
|
status=forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
@@ -694,37 +486,33 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relationto=forms.ChoiceField(
|
rel_days_relationto=forms.ChoiceField(
|
||||||
choices=choices,
|
choices=BASE_CHOICES,
|
||||||
required=False,
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relation=forms.ChoiceField(
|
rel_days_relation=forms.ChoiceField(
|
||||||
choices=BEFORE_AFTER_CHOICE,
|
choices=BEFORE_AFTER_CHOICE,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'widget' not in kwargs:
|
if 'widget' not in kwargs:
|
||||||
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=possible_choices)
|
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
|
||||||
forms.MultiValueField.__init__(
|
forms.MultiValueField.__init__(
|
||||||
self, fields=fields, require_all_fields=False, *args, **kwargs
|
self, fields=fields, require_all_fields=False, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
def set_event(self, event):
|
||||||
choices = [
|
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [
|
||||||
(c.key, c.text) for c in EVENT_BASE_CHOICES if getattr(event, c.attribute, None)
|
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||||
]
|
]
|
||||||
if self.relative_to_order:
|
|
||||||
choices += [(c.key, c.text) for c in ORDER_BASE_CHOICES]
|
|
||||||
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
|
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
return None
|
return None
|
||||||
data = reldateparts(*data_list)
|
data = reldateparts(*data_list)
|
||||||
if data.status == 'unset':
|
if data.status == 'absolute':
|
||||||
return None
|
|
||||||
elif data.status == 'absolute':
|
|
||||||
return RelativeDateWrapper(data.absolute)
|
return RelativeDateWrapper(data.absolute)
|
||||||
|
elif data.status == 'unset':
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
return RelativeDateWrapper(RelativeDate(
|
return RelativeDateWrapper(RelativeDate(
|
||||||
days=data.rel_days_number,
|
days=data.rel_days_number,
|
||||||
@@ -737,14 +525,8 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
data = reldateparts(*value)
|
data = reldateparts(*value)
|
||||||
if data.status == 'absolute' and not data.absolute:
|
if data.status == 'absolute' and not data.absolute:
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
if data.status == 'relative':
|
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
||||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
if data.rel_days_number is None or not data.rel_days_relationto:
|
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
|
||||||
elif data.rel_days_relation == "before" and not choice.supports_before:
|
|
||||||
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
|
|
||||||
elif data.rel_days_relation == "after" and not choice.supports_after:
|
|
||||||
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
|
|
||||||
|
|
||||||
return forms.MultiValueField.clean(self, value)
|
return forms.MultiValueField.clean(self, value)
|
||||||
|
|
||||||
|
|||||||
@@ -245,9 +245,6 @@ def recv_classic(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
||||||
if position.pk and position.issued_gift_cards.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
gen = event.ticket_secret_generator
|
gen = event.ticket_secret_generator
|
||||||
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
||||||
force_invalidate = True
|
force_invalidate = True
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
|
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
@@ -378,13 +377,12 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
|||||||
confirmation_code = get_random_string(8, allowed_chars="01234567890")
|
confirmation_code = get_random_string(8, allowed_chars="01234567890")
|
||||||
mail(
|
mail(
|
||||||
user.email,
|
user.email,
|
||||||
subject=gettext('Confirm event cancellation and bulk refund'),
|
subject=gettext('Bulk-refund confirmation'),
|
||||||
template='pretixbase/email/cancel_confirm.txt',
|
template='pretixbase/email/cancel_confirm.txt',
|
||||||
context={
|
context={
|
||||||
"event": str(event),
|
"event": str(event),
|
||||||
"amount": money_filter(refund_total, event.currency),
|
"amount": money_filter(refund_total, event.currency),
|
||||||
"confirmation_code": confirmation_code,
|
"confirmation_code": confirmation_code,
|
||||||
"instance": settings.PRETIX_INSTANCE_NAME,
|
|
||||||
},
|
},
|
||||||
locale=user.locale,
|
locale=user.locale,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ from django.utils.translation import (
|
|||||||
)
|
)
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -61,7 +60,7 @@ from pretix.base.models import (
|
|||||||
Seat, SeatCategoryMapping, Voucher,
|
Seat, SeatCategoryMapping, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.orders import CheckoutSession, OrderFee
|
from pretix.base.models.orders import OrderFee
|
||||||
from pretix.base.models.tax import TaxRule
|
from pretix.base.models.tax import TaxRule
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.services.checkin import _save_answers
|
from pretix.base.services.checkin import _save_answers
|
||||||
@@ -288,11 +287,11 @@ def _check_position_constraints(
|
|||||||
raise CartPositionError(error_messages['unavailable'])
|
raise CartPositionError(error_messages['unavailable'])
|
||||||
|
|
||||||
# Invalid media policy for online sale
|
# Invalid media policy for online sale
|
||||||
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||||
mt = MEDIA_TYPES[item.media_type]
|
mt = MEDIA_TYPES[item.media_type]
|
||||||
if not mt.medium_created_by_server:
|
if not mt.medium_created_by_server:
|
||||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
elif item.media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
|
elif item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
|
|
||||||
# Item removed from sales channel
|
# Item removed from sales channel
|
||||||
@@ -472,16 +471,6 @@ class CartManager:
|
|||||||
if term_last < time_machine_now(self.real_now_dt):
|
if term_last < time_machine_now(self.real_now_dt):
|
||||||
raise CartError(error_messages['payment_ended'])
|
raise CartError(error_messages['payment_ended'])
|
||||||
|
|
||||||
def _ensure_checkout_session(self):
|
|
||||||
CheckoutSession.objects.get_or_create(
|
|
||||||
event=self.event,
|
|
||||||
cart_id=self.cart_id,
|
|
||||||
defaults={
|
|
||||||
"sales_channel": self._sales_channel,
|
|
||||||
"testmode": self.event.testmode,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _extend_expiry_of_valid_existing_positions(self):
|
def _extend_expiry_of_valid_existing_positions(self):
|
||||||
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
|
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
|
||||||
# delta to reduce risk of extending already expired CartPositions.
|
# delta to reduce risk of extending already expired CartPositions.
|
||||||
@@ -927,8 +916,6 @@ class CartManager:
|
|||||||
if custom_price > 99_999_999_999:
|
if custom_price > 99_999_999_999:
|
||||||
raise CartError(error_messages['price_too_high'])
|
raise CartError(error_messages['price_too_high'])
|
||||||
|
|
||||||
custom_price = round_decimal(custom_price, currency=self.event.currency)
|
|
||||||
|
|
||||||
op = self.AddOperation(
|
op = self.AddOperation(
|
||||||
count=i['count'],
|
count=i['count'],
|
||||||
item=item,
|
item=item,
|
||||||
@@ -1051,8 +1038,6 @@ class CartManager:
|
|||||||
if custom_price > 99_999_999_999:
|
if custom_price > 99_999_999_999:
|
||||||
raise CartError(error_messages['price_too_high'])
|
raise CartError(error_messages['price_too_high'])
|
||||||
|
|
||||||
custom_price = round_decimal(custom_price, currency=self.event.currency)
|
|
||||||
|
|
||||||
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
||||||
for ca in current_addons[cp][a['item'], a['variation']]:
|
for ca in current_addons[cp][a['item'], a['variation']]:
|
||||||
if ca.listed_price != listed_price:
|
if ca.listed_price != listed_price:
|
||||||
@@ -1569,7 +1554,6 @@ class CartManager:
|
|||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
self._check_presale_dates()
|
self._check_presale_dates()
|
||||||
self._ensure_checkout_session()
|
|
||||||
self._check_max_cart_size()
|
self._check_max_cart_size()
|
||||||
|
|
||||||
err = self._delete_out_of_timeframe()
|
err = self._delete_out_of_timeframe()
|
||||||
@@ -1605,7 +1589,6 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
|
|||||||
'max_value': str(max_value) if max_value is not None else None,
|
'max_value': str(max_value) if max_value is not None else None,
|
||||||
'info_data': info_data or {},
|
'info_data': info_data or {},
|
||||||
})
|
})
|
||||||
cart_session['payments_postpone'] = False
|
|
||||||
|
|
||||||
|
|
||||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import dateutil
|
|||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
from dateutil.tz import datetime_exists
|
from dateutil.tz import datetime_exists
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||||
OuterRef, Q, Subquery, TextField, Value, When,
|
OuterRef, Q, Subquery, TextField, Value, When,
|
||||||
@@ -59,7 +59,6 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_created, periodic_task
|
from pretix.base.signals import checkin_created, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.database import conditional_atomic
|
|
||||||
from pretix.helpers.jsonlogic import Logic
|
from pretix.helpers.jsonlogic import Logic
|
||||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||||
from pretix.helpers.jsonlogic_query import (
|
from pretix.helpers.jsonlogic_query import (
|
||||||
@@ -868,15 +867,6 @@ class RequiredQuestionsError(Exception):
|
|||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
class RequiredMediaExchangeError(Exception):
|
|
||||||
def __init__(self, msg, code, media_policy, media_type):
|
|
||||||
self.msg = msg
|
|
||||||
self.code = code
|
|
||||||
self.media_policy = media_policy
|
|
||||||
self.media_type = media_type
|
|
||||||
super().__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def _save_answers(op, answers, given_answers):
|
def _save_answers(op, answers, given_answers):
|
||||||
def _create_answer(question, answer):
|
def _create_answer(question, answer):
|
||||||
try:
|
try:
|
||||||
@@ -892,7 +882,6 @@ def _save_answers(op, answers, given_answers):
|
|||||||
qa.answer = answer
|
qa.answer = answer
|
||||||
qa.save(update_fields=['answer'])
|
qa.save(update_fields=['answer'])
|
||||||
qa.options.clear()
|
qa.options.clear()
|
||||||
return qa
|
|
||||||
|
|
||||||
written = False
|
written = False
|
||||||
for q, a in given_answers.items():
|
for q, a in given_answers.items():
|
||||||
@@ -950,7 +939,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
||||||
gate=None, reusable_medium=None):
|
gate=None):
|
||||||
"""
|
"""
|
||||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||||
not valid at this time.
|
not valid at this time.
|
||||||
@@ -966,7 +955,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
:param datetime: The datetime of the checkin, defaults to now.
|
:param datetime: The datetime of the checkin, defaults to now.
|
||||||
:param simulate: If true, the check-in is not saved.
|
:param simulate: If true, the check-in is not saved.
|
||||||
:param gate: The gate the check-in was performed at.
|
:param gate: The gate the check-in was performed at.
|
||||||
:param reusable_medium: The medium that is available for an exchange
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# !!!!!!!!!
|
# !!!!!!!!!
|
||||||
@@ -1045,10 +1033,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
if not simulate:
|
if not simulate:
|
||||||
_save_answers(op, answers, given_answers)
|
_save_answers(op, answers, given_answers)
|
||||||
|
|
||||||
with conditional_atomic(not simulate):
|
with transaction.atomic():
|
||||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||||
opqs = OrderPosition.all.select_related("order", "item")
|
opqs = OrderPosition.all
|
||||||
if type != Checkin.TYPE_EXIT and not simulate:
|
if type != Checkin.TYPE_EXIT:
|
||||||
opqs = opqs.select_for_update(of=OF_SELF)
|
opqs = opqs.select_for_update(of=OF_SELF)
|
||||||
op = opqs.get(pk=op.pk)
|
op = opqs.get(pk=op.pk)
|
||||||
|
|
||||||
@@ -1113,24 +1101,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
require_answers
|
require_answers
|
||||||
)
|
)
|
||||||
|
|
||||||
required_media_policy = op.item.media_policy
|
|
||||||
required_media_type = op.item.media_type
|
|
||||||
require_a_medium = required_media_policy and required_media_type
|
|
||||||
linked_media = op.linked_media
|
|
||||||
if require_a_medium and not reusable_medium and not force:
|
|
||||||
if not linked_media.exists():
|
|
||||||
raise RequiredMediaExchangeError(
|
|
||||||
_('Ticket needs to be exchanged to a suitable medium.'),
|
|
||||||
'exchange',
|
|
||||||
required_media_policy,
|
|
||||||
required_media_type
|
|
||||||
)
|
|
||||||
elif op.organizer.settings.reusable_media_usage_enforced:
|
|
||||||
raise CheckInError(
|
|
||||||
_('This ticket has already been exchanged for a reusable medium that now needs to be used instead.'),
|
|
||||||
'already_exchanged',
|
|
||||||
)
|
|
||||||
|
|
||||||
device = None
|
device = None
|
||||||
if isinstance(auth, Device):
|
if isinstance(auth, Device):
|
||||||
device = auth
|
device = auth
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ from pretix.base.models.customers import CustomerSSOGrant
|
|||||||
|
|
||||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||||
from ..models.auth import UserKnownLoginSource
|
from ..models.auth import UserKnownLoginSource
|
||||||
from ..models.orders import CheckoutSession
|
|
||||||
from ..signals import periodic_task
|
from ..signals import periodic_task
|
||||||
|
|
||||||
|
|
||||||
@@ -44,10 +43,6 @@ def clean_cart_positions(sender, **kwargs):
|
|||||||
cp.delete()
|
cp.delete()
|
||||||
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
|
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
|
||||||
cp.delete()
|
cp.delete()
|
||||||
for cs in CheckoutSession.objects.filter(created__lt=now() - timedelta(days=14)).exclude(
|
|
||||||
Exists(CartPosition.objects.filter(cart_id=OuterRef("cart_id")))
|
|
||||||
):
|
|
||||||
cs.delete()
|
|
||||||
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
|
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
|
||||||
ia.delete()
|
ia.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from typing import List
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
||||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
from pretix.presale.views.event import get_grouped_items
|
||||||
|
|
||||||
|
|
||||||
class DummyCategory:
|
class DummyCategory:
|
||||||
@@ -162,7 +162,7 @@ class CrossSellingService:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _prepare_items(self, subevent, items_qs, discount_info):
|
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||||
items, _btn = prepare_item_list_for_shop(
|
items, _btn = get_grouped_items(
|
||||||
self.event,
|
self.event,
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
voucher=None,
|
voucher=None,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -340,13 +340,12 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
|||||||
if schedule.owner.is_active:
|
if schedule.owner.is_active:
|
||||||
mail(
|
mail(
|
||||||
email=schedule.owner.email,
|
email=schedule.owner.email,
|
||||||
subject=gettext('Scheduled export failed'),
|
subject=gettext('Export failed'),
|
||||||
template='pretixbase/email/export_failed.txt',
|
template='pretixbase/email/export_failed.txt',
|
||||||
context={
|
context={
|
||||||
'configuration_url': config_url,
|
'configuration_url': config_url,
|
||||||
'reason': msg,
|
'reason': msg,
|
||||||
'soft': soft,
|
'soft': soft,
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
|
||||||
},
|
},
|
||||||
event=context if isinstance(context, Event) else None,
|
event=context if isinstance(context, Event) else None,
|
||||||
organizer=context.organizer if isinstance(context, Event) else context,
|
organizer=context.organizer if isinstance(context, Event) else context,
|
||||||
@@ -456,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
|
|||||||
schedule,
|
schedule,
|
||||||
organizer,
|
organizer,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:organizer.export',
|
'control:organizer.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': organizer.slug,
|
'organizer': organizer.slug,
|
||||||
@@ -482,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
|
|||||||
schedule,
|
schedule,
|
||||||
event,
|
event,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:event.orders.export',
|
'control:event.orders.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
|
|||||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||||
)
|
)
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.ical import get_private_icals
|
from pretix.presale.ical import get_private_icals
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.mail')
|
logger = logging.getLogger('pretix.base.mail')
|
||||||
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
orderurl=eventreverse_absolute(
|
orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order.position', kwargs={
|
order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
event=event.name, orderurl=eventreverse_absolute(
|
event=event.name, orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order.open', kwargs={
|
order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
|
|||||||
@@ -23,13 +23,10 @@ import secrets
|
|||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.models import GiftCardAcceptance
|
||||||
from pretix.base.models import Checkin, GiftCardAcceptance, Item
|
from pretix.base.models.media import MediumKeySet
|
||||||
from pretix.base.models.media import MediumKeySet, ReusableMedium
|
|
||||||
from pretix.base.services.checkin import CheckInError
|
|
||||||
|
|
||||||
|
|
||||||
def create_nfc_mf0aes_keyset(organizer):
|
def create_nfc_mf0aes_keyset(organizer):
|
||||||
@@ -73,174 +70,3 @@ def get_keysets_for_organizer(organizer):
|
|||||||
if new_set:
|
if new_set:
|
||||||
sets.append(new_set)
|
sets.append(new_set)
|
||||||
return sets
|
return sets
|
||||||
|
|
||||||
|
|
||||||
def perform_media_exchange(organizer, media_type, identifier, link_orderposition, user, auth):
|
|
||||||
"""
|
|
||||||
Create or retrieve a medium, then link the order position to it. Expected to be called in a transaction.
|
|
||||||
|
|
||||||
:param organizer: Organizer to operate in
|
|
||||||
:param media_type: Type of medium to operate with
|
|
||||||
:param identifier: Identifier of the medium
|
|
||||||
:param link_orderposition: Position to link to the medium
|
|
||||||
:return: ReusableMedium
|
|
||||||
"""
|
|
||||||
medium = None
|
|
||||||
media_policy = link_orderposition.item.media_policy
|
|
||||||
|
|
||||||
if media_type not in MEDIA_TYPES: # should be caught by serializer already
|
|
||||||
raise CheckInError(
|
|
||||||
_('Invalid medium type.'),
|
|
||||||
Checkin.REASON_ERROR,
|
|
||||||
reason=_('Invalid medium type.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not MEDIA_TYPES[media_type].is_active(organizer):
|
|
||||||
raise CheckInError(
|
|
||||||
_('Medium type is not enabled for organizer.'),
|
|
||||||
Checkin.REASON_ERROR,
|
|
||||||
reason=_('Medium type is not enabled for organizer.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_orderposition.item.media_type != media_type:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Incorrect medium type for product.'),
|
|
||||||
Checkin.REASON_PRODUCT,
|
|
||||||
reason=_('Incorrect medium type for product.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_orderposition.linked_media.exists():
|
|
||||||
raise CheckInError(
|
|
||||||
_('Ticket is already exchanged for reusable medium.'),
|
|
||||||
Checkin.REASON_ALREADY_EXCHANGED,
|
|
||||||
reason=_('Ticket is already exchanged for reusable medium.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if media_policy in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_NEW):
|
|
||||||
link_action = "append"
|
|
||||||
else:
|
|
||||||
link_action = "replace"
|
|
||||||
|
|
||||||
if media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
|
|
||||||
try:
|
|
||||||
medium = ReusableMedium.objects.get(
|
|
||||||
type=media_type,
|
|
||||||
identifier=identifier,
|
|
||||||
organizer=organizer,
|
|
||||||
)
|
|
||||||
except ReusableMedium.DoesNotExist:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium not found.'),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if medium.is_expired or not medium.active:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium is inactive or expired.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium is inactive or expired.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif media_policy in (Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
|
||||||
try:
|
|
||||||
medium = ReusableMedium.objects.get(
|
|
||||||
type=media_type,
|
|
||||||
identifier=identifier,
|
|
||||||
organizer=organizer,
|
|
||||||
)
|
|
||||||
except ReusableMedium.DoesNotExist:
|
|
||||||
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
|
|
||||||
if not medium:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if medium.is_expired or not medium.active:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium is inactive or expired.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium is inactive or expired.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif media_policy == Item.MEDIA_POLICY_NEW:
|
|
||||||
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
|
|
||||||
except IntegrityError:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium already exists.'),
|
|
||||||
Checkin.REASON_MEDIUM_EXISTS,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not medium:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Product does not support medium exchange.'),
|
|
||||||
Checkin.REASON_PRODUCT,
|
|
||||||
reason=_('Product does not support medium exchange.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_action == 'append':
|
|
||||||
medium.linked_orderpositions.add(link_orderposition)
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': link_orderposition,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif link_action == 'replace':
|
|
||||||
already_found = False
|
|
||||||
for op_pk in medium.linked_orderpositions.values_list('pk', flat=True):
|
|
||||||
if op_pk == link_orderposition.pk:
|
|
||||||
already_found = True
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if not already_found:
|
|
||||||
medium.linked_orderpositions.set([link_orderposition])
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': link_orderposition,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
link_orderposition.order.log_action(
|
|
||||||
'pretix.reusable_medium.exchanged',
|
|
||||||
data={
|
|
||||||
'position': link_orderposition.pk,
|
|
||||||
'positionid': link_orderposition.positionid,
|
|
||||||
'medium': medium.pk,
|
|
||||||
'medium_identifier': medium.identifier,
|
|
||||||
'medium_type': medium.media_type.identifier,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
medium.touch()
|
|
||||||
|
|
||||||
return medium
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -97,8 +96,6 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
|||||||
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
|
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
|
||||||
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
|
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
|
||||||
"""
|
"""
|
||||||
if lock and not transaction.get_connection().in_atomic_block:
|
|
||||||
raise Exception('validate_memberships_in_order(lock=True) should only be called in atomic transaction!')
|
|
||||||
tz = event.timezone
|
tz = event.timezone
|
||||||
applicable_positions = [
|
applicable_positions = [
|
||||||
p for p in positions
|
p for p in positions
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ from typing import List
|
|||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _, ngettext
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
||||||
@@ -261,7 +260,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||||
# shorter. We'll see what works better in reality…
|
# shorter. We'll see what works better in reality…
|
||||||
vouchers = []
|
vouchers = []
|
||||||
codes = set()
|
|
||||||
lock_seats = []
|
lock_seats = []
|
||||||
for i, record in enumerate(data):
|
for i, record in enumerate(data):
|
||||||
try:
|
try:
|
||||||
@@ -270,14 +268,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
|
|
||||||
if not record.get("code"):
|
if not record.get("code"):
|
||||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||||
code = record.get("code")
|
|
||||||
if code.upper() in codes:
|
|
||||||
raise ValidationError(
|
|
||||||
_('Voucher codes must be unique. Code "{code}" already exists in this import.').format(
|
|
||||||
code=code,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
codes.add(code.upper())
|
|
||||||
Voucher.clean_item_properties(
|
Voucher.clean_item_properties(
|
||||||
record,
|
record,
|
||||||
event,
|
event,
|
||||||
@@ -296,22 +286,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
lock_seats.append(voucher.seat)
|
lock_seats.append(voucher.seat)
|
||||||
except (ValidationError, ImportError) as e:
|
except (ValidationError, ImportError) as e:
|
||||||
raise DataImportError(
|
raise DataImportError(
|
||||||
_('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
|
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||||
)
|
)
|
||||||
existing_codes = Voucher.objects.filter(
|
|
||||||
event=event,
|
|
||||||
code__in=codes,
|
|
||||||
).values_list("code", flat=True)
|
|
||||||
if len(existing_codes):
|
|
||||||
raise DataImportError(
|
|
||||||
ngettext(
|
|
||||||
'Voucher codes must be unique. Import contains existing voucher code {code}.',
|
|
||||||
'Voucher codes must be unique. Import contains existing voucher codes {code}.',
|
|
||||||
len(existing_codes)
|
|
||||||
).format(
|
|
||||||
code=", ".join(existing_codes)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# We don't support quotas here, so we only need to lock if seats are in use
|
# We don't support quotas here, so we only need to lock if seats are in use
|
||||||
@@ -324,13 +300,7 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
|
|
||||||
save_logentries = []
|
save_logentries = []
|
||||||
for v in vouchers:
|
for v in vouchers:
|
||||||
try:
|
v.save()
|
||||||
v.save()
|
|
||||||
except IntegrityError:
|
|
||||||
# should not happen as we check existing codes before, but we did not lock so we might have a race-condition
|
|
||||||
raise DataImportError(
|
|
||||||
_('Vouchers could not be imported, probably due to a voucher code already being in use.')
|
|
||||||
)
|
|
||||||
save_logentries.append(v.log_action(
|
save_logentries.append(v.log_action(
|
||||||
'pretix.voucher.added',
|
'pretix.voucher.added',
|
||||||
user=user,
|
user=user,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
|||||||
from pretix.base.signals import notification
|
from pretix.base.signals import notification
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.celery import get_task_priority
|
from pretix.helpers.celery import get_task_priority
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
||||||
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
|
|||||||
'site_url': settings.SITE_URL,
|
'site_url': settings.SITE_URL,
|
||||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||||
'notification': notification,
|
'notification': notification,
|
||||||
'settings_url': mainreverse_absolute(
|
'settings_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications',
|
'control:user.settings.notifications',
|
||||||
),
|
),
|
||||||
'disable_url': mainreverse_absolute(
|
'disable_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications.off',
|
'control:user.settings.notifications.off',
|
||||||
kwargs={
|
kwargs={
|
||||||
'token': user.notifications_token,
|
'token': user.notifications_token,
|
||||||
|
|||||||
+111
-191
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
|
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
||||||
Sum, Value,
|
Value,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Cast, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.db.transaction import get_connection
|
from django.db.transaction import get_connection
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -71,10 +71,10 @@ from pretix.base.models import (
|
|||||||
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||||
SeatCategoryMapping, User, Voucher,
|
SeatCategoryMapping, User, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import Event_SettingsStore, SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CheckoutSession, InvoiceAddress, OrderFee,
|
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||||
OrderRefund, generate_secret,
|
generate_secret,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||||
@@ -110,7 +110,6 @@ from pretix.celery_app import app
|
|||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
|
||||||
from pretix.testutils.middleware import debugflags_var
|
from pretix.testutils.middleware import debugflags_var
|
||||||
|
|
||||||
|
|
||||||
@@ -791,12 +790,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
[op.seat for op in sorted_positions if op.seat],
|
[op.seat for op in sorted_positions if op.seat],
|
||||||
shared_lock_objects=[event]
|
shared_lock_objects=[event]
|
||||||
)
|
)
|
||||||
elif any(cp.voucher and cp.voucher.budget for cp in sorted_positions):
|
|
||||||
# Voucher budgets are not guaranteed by the cart manager
|
|
||||||
lock_objects(
|
|
||||||
[op.voucher for op in sorted_positions if op.voucher and op.voucher.budget],
|
|
||||||
shared_lock_objects=[event]
|
|
||||||
)
|
|
||||||
|
|
||||||
q_avail = Counter()
|
q_avail = Counter()
|
||||||
v_avail = Counter()
|
v_avail = Counter()
|
||||||
@@ -961,7 +954,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
|
|
||||||
|
|
||||||
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
|
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
|
||||||
meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False):
|
meta_info: dict, event: Event, require_approval=False):
|
||||||
fees = []
|
fees = []
|
||||||
# Pre-rounding, pre-fee total is used for fee calculation
|
# Pre-rounding, pre-fee total is used for fee calculation
|
||||||
total = sum([c.gross_price_before_rounding for c in positions])
|
total = sum([c.gross_price_before_rounding for c in positions])
|
||||||
@@ -1021,14 +1014,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
|||||||
payments_assigned += to_pay
|
payments_assigned += to_pay
|
||||||
p['payment_amount'] = to_pay
|
p['payment_amount'] = to_pay
|
||||||
|
|
||||||
allow_postponed_payment = (
|
if total != payments_assigned and not require_approval:
|
||||||
require_approval or
|
|
||||||
(
|
|
||||||
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if total != payments_assigned and not allow_postponed_payment:
|
|
||||||
raise OrderError(_("The selected payment methods do not cover the total balance."))
|
raise OrderError(_("The selected payment methods do not cover the total balance."))
|
||||||
|
|
||||||
return fees
|
return fees
|
||||||
@@ -1037,8 +1023,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
|||||||
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
|
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||||
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
|
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
|
||||||
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
|
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
|
||||||
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None,
|
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None):
|
||||||
cart_id: str=None):
|
|
||||||
payments = []
|
payments = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1050,15 +1035,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
|||||||
|
|
||||||
# Final calculation of fees, also performs final rounding
|
# Final calculation of fees, also performs final rounding
|
||||||
try:
|
try:
|
||||||
fees = _apply_rounding_and_fees(
|
fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||||
positions,
|
|
||||||
payment_requests,
|
|
||||||
address,
|
|
||||||
meta_info,
|
|
||||||
event,
|
|
||||||
sales_channel=sales_channel,
|
|
||||||
require_approval=require_approval
|
|
||||||
)
|
|
||||||
except TaxRule.SaleNotAllowed:
|
except TaxRule.SaleNotAllowed:
|
||||||
raise OrderError(error_messages['country_blocked'])
|
raise OrderError(error_messages['country_blocked'])
|
||||||
|
|
||||||
@@ -1129,14 +1106,6 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
|||||||
if meta_info:
|
if meta_info:
|
||||||
for msg in meta_info.get('confirm_messages', []):
|
for msg in meta_info.get('confirm_messages', []):
|
||||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||||
if cart_id:
|
|
||||||
try:
|
|
||||||
session = CheckoutSession.objects.get(event=event, cart_id=cart_id)
|
|
||||||
except CheckoutSession.DoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
session.answers.update(order=order, checkoutsession=None)
|
|
||||||
session.delete()
|
|
||||||
|
|
||||||
order_placed.send(event, order=order, bulk=False)
|
order_placed.send(event, order=order, bulk=False)
|
||||||
return order, payments
|
return order, payments
|
||||||
@@ -1184,7 +1153,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
|
|||||||
|
|
||||||
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
||||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
||||||
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None, cart_id: str=None):
|
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None):
|
||||||
for p in payment_requests:
|
for p in payment_requests:
|
||||||
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
||||||
if not p['pprov']:
|
if not p['pprov']:
|
||||||
@@ -1291,7 +1260,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
valid_if_pending=valid_if_pending,
|
valid_if_pending=valid_if_pending,
|
||||||
api_meta=api_meta,
|
api_meta=api_meta,
|
||||||
tax_rounding_mode=tax_rounding_mode,
|
tax_rounding_mode=tax_rounding_mode,
|
||||||
cart_id=cart_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1519,104 +1487,83 @@ def send_expiry_warnings(sender, **kwargs):
|
|||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def send_download_reminders(sender, **kwargs):
|
def send_download_reminders(sender, **kwargs):
|
||||||
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
qs = Order.objects.annotate(
|
||||||
events = Event.objects.filter(
|
first_date=Coalesce(
|
||||||
Q(has_subevents=False, date_from__gte=now()) |
|
Min('all_positions__subevent__date_from'),
|
||||||
(Q(has_subevents=True) & Q(Exists(
|
F('event__date_from')
|
||||||
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
|
|
||||||
)))
|
|
||||||
).annotate(
|
|
||||||
reminder_days=Subquery(
|
|
||||||
Event_SettingsStore.objects.filter(
|
|
||||||
object=OuterRef('id'),
|
|
||||||
key='mail_days_download_reminder'
|
|
||||||
).exclude(
|
|
||||||
value="None"
|
|
||||||
).annotate(
|
|
||||||
val=Cast(F("value"), output_field=models.IntegerField()),
|
|
||||||
).values("val")
|
|
||||||
)
|
)
|
||||||
).filter(
|
).filter(
|
||||||
reminder_days__isnull=False,
|
download_reminder_sent=False,
|
||||||
).order_by()
|
datetime__lte=now() - timedelta(hours=2),
|
||||||
|
first_date__gte=today,
|
||||||
|
).only(
|
||||||
|
'pk', 'event_id', 'sales_channel', 'datetime',
|
||||||
|
).order_by('event_id')
|
||||||
|
event_id = None
|
||||||
|
days = None
|
||||||
|
event = None
|
||||||
|
|
||||||
for event in events.iterator(chunk_size=10_000):
|
for o in qs:
|
||||||
qs = event.orders.filter(
|
if o.event_id != event_id:
|
||||||
download_reminder_sent=False,
|
days = o.event.settings.get('mail_days_download_reminder', as_type=int)
|
||||||
datetime__lte=now() - timedelta(hours=2),
|
event = o.event
|
||||||
)
|
event_id = o.event_id
|
||||||
|
|
||||||
if event.has_subevents:
|
if days is None:
|
||||||
qs = qs.annotate(
|
continue
|
||||||
first_date=Min('all_positions__subevent__date_from')
|
|
||||||
).filter(
|
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||||
Q(first_date__gte=today)
|
continue
|
||||||
)
|
|
||||||
else:
|
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
if now() < reminder_date or o.datetime > reminder_date:
|
||||||
if now() < event_reminder_date:
|
continue
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
||||||
|
if o.download_reminder_sent:
|
||||||
|
# Race condition
|
||||||
|
continue
|
||||||
|
positions = list(o.positions_with_tickets)
|
||||||
|
if not positions:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
qs = qs.only(
|
if not o.ticket_download_available:
|
||||||
'pk', 'event_id', 'sales_channel', 'datetime',
|
|
||||||
).order_by()
|
|
||||||
|
|
||||||
for o in qs:
|
|
||||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if event.has_subevents:
|
if o.status != Order.STATUS_PAID:
|
||||||
reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||||
else:
|
|
||||||
reminder_date = event_reminder_date
|
|
||||||
if now() < reminder_date or o.datetime > reminder_date:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
|
||||||
if o.download_reminder_sent:
|
|
||||||
# Race condition
|
|
||||||
continue
|
|
||||||
positions = list(o.positions_with_tickets)
|
|
||||||
if not positions:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not o.ticket_download_available:
|
with language(o.locale, o.event.settings.region):
|
||||||
continue
|
o.download_reminder_sent = True
|
||||||
|
o.save(update_fields=['download_reminder_sent'])
|
||||||
|
email_template = event.settings.mail_text_download_reminder
|
||||||
|
email_subject = event.settings.mail_subject_download_reminder
|
||||||
|
email_context = get_email_context(event=event, order=o)
|
||||||
|
o.send_mail(
|
||||||
|
email_subject, email_template, email_context,
|
||||||
|
'pretix.event.order.email.download_reminder_sent',
|
||||||
|
attach_tickets=True
|
||||||
|
)
|
||||||
|
|
||||||
if o.status != Order.STATUS_PAID:
|
if event.settings.mail_send_download_reminder_attendee:
|
||||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
for p in positions:
|
||||||
continue
|
if p.subevent_id:
|
||||||
|
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||||
with language(o.locale, o.event.settings.region):
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
o.download_reminder_sent = True
|
)
|
||||||
o.save(update_fields=['download_reminder_sent'])
|
if now() < reminder_date:
|
||||||
email_template = event.settings.mail_text_download_reminder
|
continue
|
||||||
email_subject = event.settings.mail_subject_download_reminder
|
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
||||||
email_context = get_email_context(event=event, order=o)
|
email_template = event.settings.mail_text_download_reminder_attendee
|
||||||
o.send_mail(
|
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||||
email_subject, email_template, email_context,
|
email_context = get_email_context(event=event, order=o, position=p)
|
||||||
'pretix.event.order.email.download_reminder_sent',
|
o.send_mail(
|
||||||
attach_tickets=True
|
email_subject, email_template, email_context,
|
||||||
)
|
'pretix.event.order.email.download_reminder_sent',
|
||||||
|
attach_tickets=True, position=p
|
||||||
if event.settings.mail_send_download_reminder_attendee:
|
)
|
||||||
for p in positions:
|
|
||||||
if p.subevent_id:
|
|
||||||
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
|
|
||||||
hour=0, minute=0, second=0, microsecond=0
|
|
||||||
)
|
|
||||||
if now() < reminder_date:
|
|
||||||
continue
|
|
||||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
|
||||||
email_template = event.settings.mail_text_download_reminder_attendee
|
|
||||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
|
||||||
email_context = get_email_context(event=event, order=o, position=p)
|
|
||||||
o.send_mail(
|
|
||||||
email_subject, email_template, email_context,
|
|
||||||
'pretix.event.order.email.download_reminder_sent',
|
|
||||||
attach_tickets=True, position=p
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||||
@@ -1652,7 +1599,6 @@ class OrderChangeManager:
|
|||||||
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
||||||
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
||||||
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
||||||
'gift_card_secret': gettext_lazy('You cannot change the ticket secret of a position that has been used to issue a gift card.'),
|
|
||||||
'max_items_per_product': ngettext_lazy(
|
'max_items_per_product': ngettext_lazy(
|
||||||
"You cannot select more than %(max)s item of the product %(product)s.",
|
"You cannot select more than %(max)s item of the product %(product)s.",
|
||||||
"You cannot select more than %(max)s items of the product %(product)s.",
|
"You cannot select more than %(max)s items of the product %(product)s.",
|
||||||
@@ -1810,9 +1756,6 @@ class OrderChangeManager:
|
|||||||
self._operations.append(self.RegenerateSecretOperation(position))
|
self._operations.append(self.RegenerateSecretOperation(position))
|
||||||
|
|
||||||
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
||||||
if position.issued_gift_cards.exists():
|
|
||||||
raise OrderError(self.error_messages['gift_card_secret'])
|
|
||||||
|
|
||||||
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
||||||
|
|
||||||
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
||||||
@@ -2000,18 +1943,13 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
||||||
``"count"``, and ``"price"``.
|
``"count"``, and ``"price"``.
|
||||||
:param limit_main_positions: By default, the method works on all positions of the order. If you set this to a
|
:param limit_main_positions: By default, the method works on all methods of the order. If you set this to a
|
||||||
queryset or a list of positions, all other positions and their add-ons will be kept
|
queryset or a list of positions, all other positions and their add-ons will be kept
|
||||||
untouched.
|
untouched.
|
||||||
"""
|
"""
|
||||||
if self._operations:
|
if self._operations:
|
||||||
raise ValueError("Setting addons should be the first/only operation")
|
raise ValueError("Setting addons should be the first/only operation")
|
||||||
|
|
||||||
def _allowed_on_order_sales_channel(item_or_var, order):
|
|
||||||
return item_or_var.all_sales_channels or (
|
|
||||||
order.sales_channel.identifier in (s.identifier for s in item_or_var.limit_sales_channels.all())
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prepare containers for min/max check of products
|
# Prepare containers for min/max check of products
|
||||||
item_counts = Counter()
|
item_counts = Counter()
|
||||||
for p in self.order.positions.all():
|
for p in self.order.positions.all():
|
||||||
@@ -2105,11 +2043,13 @@ class OrderChangeManager:
|
|||||||
if not item.is_available() or (variation and not variation.is_available()):
|
if not item.is_available() or (variation and not variation.is_available()):
|
||||||
raise OrderError(error_messages['unavailable'])
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
if not _allowed_on_order_sales_channel(item, self.order):
|
if not item.all_sales_channels:
|
||||||
raise OrderError(error_messages['unavailable'])
|
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 _allowed_on_order_sales_channel(variation, self.order):
|
if variation and not variation.all_sales_channels:
|
||||||
raise OrderError(error_messages['unavailable'])
|
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||||
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
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'])
|
raise OrderError(error_messages['not_for_sale'])
|
||||||
@@ -2157,36 +2097,6 @@ class OrderChangeManager:
|
|||||||
)
|
)
|
||||||
item_counts[item] += 1
|
item_counts[item] += 1
|
||||||
|
|
||||||
def _addon_is_available(a):
|
|
||||||
# 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 set voucher=None because that's what's done when generating the form in
|
|
||||||
# OrderChangeMixin (vouchers for addons are not supported).
|
|
||||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
|
||||||
# be part of the input.
|
|
||||||
if not _allowed_on_order_sales_channel(a.item, self.order) or (
|
|
||||||
a.variation and not _allowed_on_order_sales_channel(a.variation, self.order)
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
items, _ = prepare_item_list_for_shop(
|
|
||||||
self.order.event,
|
|
||||||
channel=self.order.sales_channel,
|
|
||||||
subevent=a.subevent,
|
|
||||||
voucher=None,
|
|
||||||
base_qs=Item.objects.filter(pk=a.item.pk),
|
|
||||||
allow_addons=True
|
|
||||||
)
|
|
||||||
if (not items) or items[0].current_unavailability_reason:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if a.variation:
|
|
||||||
variations = [var for var in items[0].available_variations if var.pk == a.variation.pk]
|
|
||||||
if (not variations) or variations[0].current_unavailability_reason:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Detect removed add-ons and create RemoveOperations
|
# Detect removed add-ons and create RemoveOperations
|
||||||
for cp, al in list(current_addons.items()):
|
for cp, al in list(current_addons.items()):
|
||||||
for k, v in al.items():
|
for k, v in al.items():
|
||||||
@@ -2196,7 +2106,22 @@ class OrderChangeManager:
|
|||||||
for a in current_addons[cp][k][:current_num - input_num]:
|
for a in current_addons[cp][k][:current_num - input_num]:
|
||||||
if a.canceled:
|
if a.canceled:
|
||||||
continue
|
continue
|
||||||
if not _addon_is_available(a):
|
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 a.item.all_sales_channels and
|
||||||
|
not a.item.limit_sales_channels.contains(self.order.sales_channel)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if is_unavailable:
|
||||||
# "Re-select" add-on
|
# "Re-select" add-on
|
||||||
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
||||||
continue
|
continue
|
||||||
@@ -3215,12 +3140,12 @@ class OrderChangeManager:
|
|||||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
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,
|
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,
|
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
|
||||||
api_meta: dict=None, cart_id: str=None):
|
api_meta: dict=None):
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||||
sales_channel, shown_total, customer, api_meta, cart_id=cart_id)
|
sales_channel, shown_total, customer, api_meta)
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
except (MaxRetriesExceededError, LockTimeoutException):
|
except (MaxRetriesExceededError, LockTimeoutException):
|
||||||
@@ -3484,7 +3409,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
new_invoice = None
|
new_invoice_created = False
|
||||||
if recreate_invoices:
|
if recreate_invoices:
|
||||||
# Lock to prevent duplicate invoice creation
|
# Lock to prevent duplicate invoice creation
|
||||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||||
@@ -3495,16 +3420,13 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
|||||||
if has_active_invoice and order.total != oldtotal:
|
if has_active_invoice and order.total != oldtotal:
|
||||||
try:
|
try:
|
||||||
generate_cancellation(i)
|
generate_cancellation(i)
|
||||||
new_invoice = generate_invoice(order)
|
generate_invoice(order)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Could not generate invoice.")
|
logger.exception("Could not generate invoice.")
|
||||||
order.log_action("pretix.event.order.invoice.failed", data={
|
order.log_action("pretix.event.order.invoice.failed", data={
|
||||||
"exception": str(e)
|
"exception": str(e)
|
||||||
})
|
})
|
||||||
else:
|
new_invoice_created = True
|
||||||
order.log_action('pretix.event.order.invoice.generated', data={
|
|
||||||
'invoice': new_invoice.pk
|
|
||||||
})
|
|
||||||
|
|
||||||
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
|
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
|
||||||
if order.event.settings.get('invoice_generate') == 'True' or (
|
if order.event.settings.get('invoice_generate') == 'True' or (
|
||||||
@@ -3514,9 +3436,10 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
|||||||
try:
|
try:
|
||||||
if has_active_invoice:
|
if has_active_invoice:
|
||||||
generate_cancellation(i)
|
generate_cancellation(i)
|
||||||
new_invoice = generate_invoice(order)
|
i = generate_invoice(order)
|
||||||
|
new_invoice_created = True
|
||||||
order.log_action('pretix.event.order.invoice.generated', data={
|
order.log_action('pretix.event.order.invoice.generated', data={
|
||||||
'invoice': new_invoice.pk
|
'invoice': i.pk
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Could not generate invoice.")
|
logger.exception("Could not generate invoice.")
|
||||||
@@ -3524,11 +3447,8 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
|||||||
"exception": str(e)
|
"exception": str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
if new_invoice and invoice_transmission_separately(new_invoice):
|
|
||||||
transmit_invoice.apply_async(args=(order.event_id, new_invoice.pk, False))
|
|
||||||
|
|
||||||
order.create_transactions()
|
order.create_transactions()
|
||||||
return old_fee, new_fee, fee, new_payment, bool(new_invoice)
|
return old_fee, new_fee, fee, new_payment, new_invoice_created
|
||||||
|
|
||||||
|
|
||||||
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
||||||
@@ -3586,7 +3506,7 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
|
|||||||
from pretix.base.models import ReusableMedium
|
from pretix.base.models import ReusableMedium
|
||||||
|
|
||||||
for p in order.positions.all():
|
for p in order.positions.all():
|
||||||
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||||
mt = MEDIA_TYPES[p.item.media_type]
|
mt = MEDIA_TYPES[p.item.media_type]
|
||||||
if mt.medium_created_by_server and not p.linked_media.exists():
|
if mt.medium_created_by_server and not p.linked_media.exists():
|
||||||
rm = ReusableMedium.objects.create(
|
rm = ReusableMedium.objects.create(
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from datetime import datetime, time, timedelta
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
from django.utils.timezone import make_aware, now
|
|
||||||
|
|
||||||
from pretix.base.models import Event, SalesChannel
|
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
|
||||||
|
|
||||||
|
|
||||||
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
|
|
||||||
now_dt = now_dt or now()
|
|
||||||
tz = ZoneInfo(event.settings.timezone)
|
|
||||||
|
|
||||||
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
|
|
||||||
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
|
|
||||||
mode = event.settings.get('payment_term_mode')
|
|
||||||
sales_channel_suffix = ""
|
|
||||||
|
|
||||||
if mode == 'days':
|
|
||||||
exp_by_date = now_dt.astimezone(tz) + timedelta(
|
|
||||||
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
|
|
||||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
|
||||||
if event.settings.get('payment_term_weekdays'):
|
|
||||||
if exp_by_date.weekday() == 5:
|
|
||||||
exp_by_date += timedelta(days=2)
|
|
||||||
elif exp_by_date.weekday() == 6:
|
|
||||||
exp_by_date += timedelta(days=1)
|
|
||||||
elif mode == 'minutes':
|
|
||||||
exp_by_date = now_dt.astimezone(tz) + timedelta(
|
|
||||||
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
|
|
||||||
else:
|
|
||||||
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
|
|
||||||
|
|
||||||
expires = exp_by_date
|
|
||||||
|
|
||||||
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
|
||||||
if term_last:
|
|
||||||
if event.has_subevents and subevents:
|
|
||||||
terms = [
|
|
||||||
term_last.datetime(se).date()
|
|
||||||
for se in subevents
|
|
||||||
]
|
|
||||||
if not terms:
|
|
||||||
return expires
|
|
||||||
term_last = min(terms)
|
|
||||||
else:
|
|
||||||
term_last = term_last.datetime(event).date()
|
|
||||||
term_last = make_aware(datetime.combine(
|
|
||||||
term_last,
|
|
||||||
time(hour=23, minute=59, second=59)
|
|
||||||
), tz)
|
|
||||||
if term_last < expires:
|
|
||||||
return term_last
|
|
||||||
|
|
||||||
return expires
|
|
||||||
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
|
|||||||
|
|
||||||
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
||||||
def base_placeholders(sender, **kwargs):
|
def base_placeholders(sender, **kwargs):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
def _event_sample(event):
|
def _event_sample(event):
|
||||||
if event.has_subevents:
|
if event.has_subevents:
|
||||||
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
lambda event: LazyDate(now() + timedelta(days=15))
|
lambda event: LazyDate(now() + timedelta(days=15))
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_secret()
|
'hash': order.email_confirm_secret()
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['order', 'event'],
|
'url_button', ['order', 'event'],
|
||||||
url_func=lambda order, event: eventreverse_absolute(
|
url_func=lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda order, event: _("View order details"),
|
text_func=lambda order, event: _("View order details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View order details"),
|
sample_text_func=lambda event: _("View order details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
|
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position',
|
'presale:event.order.position',
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['event', 'position'],
|
'url_button', ['event', 'position'],
|
||||||
url_func=lambda event, position: eventreverse_absolute(
|
url_func=lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda event, position: _("View registration details"),
|
text_func=lambda event, position: _("View registration details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View registration details"),
|
sample_text_func=lambda event: _("View registration details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -561,22 +561,6 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||||
lambda event: str(event.location or ''),
|
lambda event: str(event.location or ''),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
|
||||||
'event_begin_time', ['event_or_subevent'],
|
|
||||||
lambda event_or_subevent:
|
|
||||||
date_format(event_or_subevent.date_from.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
|
||||||
if event_or_subevent.date_from
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(event.date_from.astimezone(event.timezone), 'TIME_FORMAT') if event.date_from else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalTextPlaceholder(
|
|
||||||
'event_end_time', ['event_or_subevent'],
|
|
||||||
lambda event_or_subevent:
|
|
||||||
date_format(event_or_subevent.date_to.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
|
||||||
if event_or_subevent.date_to
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(event.date_to.astimezone(event.timezone), 'TIME_FORMAT') if event.date_to else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event_admission_time', ['event_or_subevent'],
|
'event_admission_time', ['event_or_subevent'],
|
||||||
lambda event_or_subevent:
|
lambda event_or_subevent:
|
||||||
@@ -597,20 +581,20 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_remove', ['waiting_list_voucher', 'event'],
|
'url_remove', ['waiting_list_voucher', 'event'],
|
||||||
lambda waiting_list_voucher, event: eventreverse_absolute(
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
event, 'presale:event.waitinglist.remove'
|
event, 'presale:event.waitinglist.remove'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.waitinglist.remove',
|
'presale:event.waitinglist.remove',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['waiting_list_voucher', 'event'],
|
'url', ['waiting_list_voucher', 'event'],
|
||||||
lambda waiting_list_voucher, event: eventreverse_absolute(
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.redeem',
|
'presale:event.redeem',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
@@ -627,7 +611,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
order.full_code,
|
order.full_code,
|
||||||
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -639,7 +623,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
), lambda event: '\n' + '\n\n'.join(
|
), lambda event: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||||
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order['code'],
|
'order': order['code'],
|
||||||
@@ -678,13 +662,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
lambda event, voucher_list: ' \n'.join([
|
lambda event, voucher_list: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in voucher_list
|
for c in voucher_list
|
||||||
]),
|
]),
|
||||||
lambda event: ' \n'.join([
|
lambda event: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
@@ -692,10 +676,10 @@ def base_placeholders(sender, **kwargs):
|
|||||||
inline=False,
|
inline=False,
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: eventreverse_absolute(event, 'presale:event.index', kwargs={
|
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
}), lambda event: eventreverse_absolute(event, 'presale:event.index', kwargs={
|
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
})
|
})
|
||||||
@@ -817,10 +801,11 @@ def get_available_placeholders(event, base_parameters, rich=False):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def prepare_sample_context_for_preview(placeholder_to_sample):
|
def get_sample_context(event, context_parameters, rich=True):
|
||||||
context_dict = {}
|
context_dict = {}
|
||||||
lbl = _('This value will be replaced based on dynamic parameters.')
|
lbl = _('This value will be replaced based on dynamic parameters.')
|
||||||
for k, sample in placeholder_to_sample.items():
|
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
|
||||||
|
sample = v.render_sample(event)
|
||||||
if isinstance(sample, PlainHtmlAlternativeString):
|
if isinstance(sample, PlainHtmlAlternativeString):
|
||||||
context_dict[k] = PlainHtmlAlternativeString(
|
context_dict[k] = PlainHtmlAlternativeString(
|
||||||
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
||||||
@@ -845,12 +830,3 @@ def prepare_sample_context_for_preview(placeholder_to_sample):
|
|||||||
escape(sample)
|
escape(sample)
|
||||||
))
|
))
|
||||||
return context_dict
|
return context_dict
|
||||||
|
|
||||||
|
|
||||||
def get_sample_context(event, context_parameters, rich=True):
|
|
||||||
return prepare_sample_context_for_preview(
|
|
||||||
{
|
|
||||||
k: v.render_sample(event)
|
|
||||||
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class QuotaAvailability:
|
|||||||
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
|
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
|
||||||
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
|
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
|
||||||
return
|
return
|
||||||
rc.set(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', ex=10)
|
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10)
|
||||||
|
|
||||||
update = defaultdict(list)
|
update = defaultdict(list)
|
||||||
for q in quotas:
|
for q in quotas:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user